Skip to content

[codex] add evm-only staking precompile#3616

Open
codchen wants to merge 5 commits into
codex/sei-v3-evm-only-scaffoldfrom
codex/evmonly-staking-precompile
Open

[codex] add evm-only staking precompile#3616
codchen wants to merge 5 commits into
codex/sei-v3-evm-only-scaffoldfrom
codex/evmonly-staking-precompile

Conversation

@codchen

@codchen codchen commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the first SDK-free custom precompile for the evm-only executor: staking at 0x0000000000000000000000000000000000001005.

This PR wires custom precompile execution into the evm-only executor, stores precompile module-like state as storage owned by the precompile address, and adds an end-block hook for staking validator-set updates and delayed redelegation/undelegation completion. It also keeps staking token handling explicitly usei-only for the evm-only path.

Details

  • Adds SDK-free precompile context interfaces for byte-keyed store access, native balance transfers, logs, and end-block execution.
  • Implements staking create/delegate/redelegate/undelegate/query flows without Cosmos keepers or SDK objects.
  • Uses a deterministic escrow address for bonded stake instead of the precompile account balance.
  • Replicates staking end-block behavior with explicit JSON indexes instead of store iterators.
  • Moves reusable JSON/event/helper utilities under giga/evmonly/precompiles/util.
  • Adds executor and staking tests, including a delegate -> redelegate -> undelegate lifecycle e2e that checks balances and delegation accounting through maturity.

Validation

  • go test ./giga/evmonly/...

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJun 30, 2026, 6:12 AM

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.98821% with 933 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.15%. Comparing base (f1fa418) to head (accacbf).

Files with missing lines Patch % Lines
giga/evmonly/precompiles/staking/staking.go 24.31% 353 Missing and 86 partials ⚠️
giga/evmonly/precompiles/staking/state.go 57.14% 142 Missing and 68 partials ⚠️
giga/evmonly/precompiles/staking/endblock.go 37.39% 107 Missing and 47 partials ⚠️
giga/evmonly/precompile_adapter.go 75.00% 25 Missing and 21 partials ⚠️
giga/evmonly/precompiles/staking/commission.go 59.37% 17 Missing and 9 partials ⚠️
giga/evmonly/precompiles/staking/helpers.go 40.00% 18 Missing and 6 partials ⚠️
giga/evmonly/precompiles/util/helpers.go 51.72% 7 Missing and 7 partials ⚠️
giga/evmonly/precompiles/staking/balances.go 60.00% 4 Missing and 4 partials ⚠️
giga/evmonly/precompiles/util/events.go 71.42% 2 Missing and 2 partials ⚠️
giga/evmonly/precompiles/util/json.go 71.42% 2 Missing and 2 partials ⚠️
... and 2 more
Additional details and impacted files

Impacted file tree graph

@@                        Coverage Diff                         @@
##           codex/sei-v3-evm-only-scaffold    #3616      +/-   ##
==================================================================
- Coverage                           58.27%   58.15%   -0.13%     
==================================================================
  Files                                2176     2187      +11     
  Lines                              176783   178461    +1678     
==================================================================
+ Hits                               103024   103781     +757     
- Misses                              64710    65383     +673     
- Partials                             9049     9297     +248     
Flag Coverage Δ
sei-chain-pr 56.41% <44.98%> (-19.33%) ⬇️
sei-db 70.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
giga/evmonly/executor.go 85.62% <80.00%> (-0.10%) ⬇️
giga/evmonly/precompiles/staking/events.go 60.00% <60.00%> (ø)
giga/evmonly/precompiles/util/events.go 71.42% <71.42%> (ø)
giga/evmonly/precompiles/util/json.go 71.42% <71.42%> (ø)
giga/evmonly/precompiles/staking/balances.go 60.00% <60.00%> (ø)
giga/evmonly/precompiles/util/helpers.go 51.72% <51.72%> (ø)
giga/evmonly/precompiles/staking/helpers.go 40.00% <40.00%> (ø)
giga/evmonly/precompiles/staking/commission.go 59.37% <59.37%> (ø)
giga/evmonly/precompile_adapter.go 75.00% <75.00%> (ø)
giga/evmonly/precompiles/staking/endblock.go 37.39% <37.39%> (ø)
... and 2 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codchen codchen requested review from arajasek and philipsu522 June 22, 2026 08:43
@codchen codchen marked this pull request as ready for review June 22, 2026 08:44
@cursor

cursor Bot commented Jun 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Adds consensus-facing validator set updates and staking accounting on the evmonly path; incorrect end-block or escrow/balance logic could affect bonded stake and validator power despite extensive tests.

Overview
Introduces the first working custom precompile on the EVM-only path: staking at 0x0000000000000000000000000000000000001005, without Cosmos keepers or sdk.Context.

The executor now runs registered precompile contracts (not only fail-closed placeholders) via a new adapter that builds a precompiles.Context with byte-keyed storage mapped to EVM slots on the precompile address, native balance transfers, and logs. After all txs, it invokes EndBlock on precompiles that implement it and returns ValidatorUpdates on BlockResult. Empty blocks still execute when a custom precompile registry is configured so end-block logic can run. OCC parallel execution is disabled whenever any custom precompile is registered.

The precompile API splits module state into Store and BalanceTransfer (replacing a single State interface) and adds EndBlocker plus ValidatorUpdate.

Staking implements create/edit validator, delegate, redelegate, undelegate, queries, commission checks, JSON-backed indexes, and end-block validator-set reconciliation, mature unbonding/redelegation, and historical info—payable stake goes to a deterministic escrow address in usei. Unimplemented registry addresses still return ErrCustomPrecompilesOpen.

Documentation and broad executor e2e / staking unit tests cover payable forwarding and a full delegation lifecycle through maturity.

Reviewed by Cursor Bugbot for commit accacbf. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread giga/evmonly/precompiles/staking/state.go
Comment thread giga/evmonly/precompiles/staking/staking.go
Comment thread giga/evmonly/precompiles/staking/staking.go
@codchen codchen force-pushed the codex/sei-v3-evm-only-scaffold branch 2 times, most recently from ab82ec3 to 23fc6d3 Compare June 24, 2026 03:53
record, ok, err := getUnbondingDelegation(ctx.Store, pair.DelegatorAddress, pair.ValidatorAddress)
if err != nil || !ok {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unbonding record skips payout

Medium Severity

In completeUnbonding and completeRedelegation, when the store lookup returns ok == false with a nil error, the functions return success instead of failing. The mature-queue loop still deletes queue entries, so a queued unbonding pair without a matching record can be dropped without releasing escrowed stake or cleaning redelegation indexes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b90eb37. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is consistent with cosmos behavior

@codchen codchen force-pushed the codex/evmonly-staking-precompile branch 2 times, most recently from 27e3acc to 550f6fa Compare June 29, 2026 12:34

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the first SDK-free staking custom precompile for the evm-only executor, with byte-keyed storage-backed state, an end-block hook, and a thorough test suite including a full delegate→redelegate→undelegate lifecycle. The implementation closely mirrors Cosmos staking semantics and looks correct; the main open concern is flat gas accounting for state-growing operations. No blocking correctness bugs found.

Findings: 0 blocking | 9 non-blocking | 4 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Gas accounting (Codex finding #1): custom precompile execution charges only the flat RequiredGas (writeGas=20000 + 16/byte). Staking writes JSON-encoded indexes (validators index, per-validator/per-delegator delegation lists, queues) that are re-read, sorted, and re-serialized into chunked storage slots on every mutation — O(n) SSTOREs charged as a flat fee. Block gas therefore does not bound precompile state growth or runtime, allowing cheap unbounded index growth. This mirrors existing Sei precompile flat-gas behavior and the evm-only path is documented as early-integration, so non-blocking — but should be revisited before production (per-operation/per-byte-of-stored-state metering).
  • Disagree with Codex finding #2 (editValidator minSelfDelegation vs validator.Tokens): Cosmos x/staking msg_server.EditValidator also checks MinSelfDelegation.GT(validator.Tokens), not self-delegation. The implementation here is faithful Cosmos parity, not a new invariant break.
  • Disagree with Codex finding #3 (commission max-change uses signed delta, not abs): Cosmos Commission.ValidateNewRate uses signed newRate.Sub(c.Rate).GT(MaxChangeRate) and also permits unbounded decreases. The code (and its comment) intentionally matches upstream, so this is parity rather than a bug.
  • Cursor second-opinion review file (cursor-review.md) is empty — that pass produced no output. REVIEW_GUIDELINES.md is also empty/absent, so no repo-specific standards were applied.
  • Documented limitations (no rewards/slashing/jailing, shares track tokens 1:1, historical info recorded in end-block rather than begin-block) are clearly noted in the README; just confirm downstream consumers/indexers tolerate the zero-amount DelegationRewardsWithdrawn events and the deferred historical-info availability.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

if isTransaction(method.Name) {
gas = writeGas
}
return gas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Gas is flat per call (writeGas + 16/byte of input) regardless of how much module-like state the operation rewrites. delegate/redelegate/undelegate rewrite O(n) JSON index lists (validators index, per-validator/per-delegator delegation lists, time queues) into chunked storage slots, so the real SSTORE/CPU cost can far exceed the charged gas, and block gas no longer bounds state growth. This matches existing Sei precompile flat-gas semantics and the path is experimental, so not blocking — but worth per-operation metering before this backs production state. (Codex finding #1.)

if err != nil {
return nil, err
}
if minSelfDelegation.Cmp(tokens) > 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Codex flags this as comparing minSelfDelegation against validator.Tokens (which includes third-party delegations). Note this matches Cosmos x/staking msg_server.EditValidator, which also checks msg.MinSelfDelegation.GT(validator.Tokens). So this is faithful parity, not a newly introduced invariant break — no change needed unless intentional divergence from Cosmos is desired here.

return errCommissionNegative
case newRate.Cmp(maxRate) > 0:
return errCommissionGTMaxRate
case new(big.Rat).Sub(newRate, oldRate).Cmp(maxChange) > 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Codex flags that large commission decreases bypass the max-change check because this uses signed newRate - oldRate rather than the absolute delta. This actually matches Cosmos Commission.ValidateNewRate, which likewise uses the signed delta and allows unbounded decreases. Given the stated goal of Cosmos parity (and the function doc), this is correct as written — not a bug.

} else if err := addPoolNotBonded(ctx.Store, useiAmount); err != nil {
return nil, err
}
p.emit(ctx, "Delegate", ctx.Caller, validatorAddress, util.CloneBig(ctx.ApparentValue))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Minor unit inconsistency: the Delegate event emits the raw wei ApparentValue (18 decimals), while Undelegate (line 392) and Redelegate (line 334) emit the usei amount arg (6 decimals). Worth confirming this matches the canonical Sei staking precompile event semantics so indexers decode all three consistently.

codchen and others added 5 commits June 30, 2026 14:11
Address review findings on the SDK-free staking precompile:

- redelegate/undelegate now enforce Cosmos guardrails: reject
  self-redelegation, transitive redelegation, and over-MaxEntries
  unbonding/redelegation entries.
- delegation shares are reported as an sdk.Dec (scaled by 10^precision)
  to match the keeper-backed precompile.
- create/editValidator validate commission bounds, the 24h rate-change
  rule, and min-self-delegation increase/cap; stricter decimal parsing
  rejects fraction/scientific forms.
- historicalInfo query is read-only again; historical info is tracked
  and pruned in the end-block hook.
- powerReduction set to 1_000_000 (Sei DefaultPowerReduction); tests use
  SEI-scale stakes.
- validators query no longer re-reads validators; README documents the
  staking parity limitations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codchen codchen force-pushed the codex/evmonly-staking-precompile branch from 550f6fa to accacbf Compare June 30, 2026 06:11

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit accacbf. Configure here.

}
if err := setHistoricalInfo(ctx.Store, ctx.Block.Number); err != nil {
return nil, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historical param ignored on writes

Low Severity

createValidator and editValidator always call setHistoricalInfo during the transaction, while end-block trackHistoricalInfo skips recording when HistoricalEntries is zero. With historical tracking disabled via params, validator create/edit can still persist and index historical snapshots, diverging from the intended Cosmos-style behavior.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit accacbf. Configure here.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a substantial, well-tested SDK-free staking precompile for the evm-only executor. The main blocker is that createValidator accepts an arbitrary/empty consensus pubkey, which can later be emitted as an invalid consensus ValidatorUpdate; there are also a few divergences from Cosmos staking semantics (tie-break ordering, unenforced MaxVotingPowerRatio) and a flat gas model worth addressing.

Findings: 1 blocking | 7 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • MaxVotingPowerRatio / MaxVotingPowerEnforcementThreshold are surfaced in Params but never enforced anywhere in delegate/createValidator/redelegate/endblock, so a validator can accumulate more than the configured maximum voting-power share. Defaults are "0" (disabled) so there's no immediate impact, but if a non-zero ratio is ever loaded it is silently ignored. (Raised by Codex.) Either enforce it or document the gap explicitly in README's limitations list.
  • Gas is charged as a flat readGas/writeGas plus per-input-byte, independent of how much state is actually written. Mutating handlers rewrite whole JSON blobs and re-sort/rewrite entire string-list indexes (validators/index, delegator/validator delegation indexes, queues) — O(n) storage slots for a fixed 20000 gas. On a live chain this underprices unbounded state growth and is a potential DoS vector; consider metering by bytes/slots written before this path is production-wired.
  • util.EmitEvent silently swallows Pack errors (returns without emitting), so a future event/ABI mismatch would drop logs with no signal. A test or at least a comment documenting the intentional best-effort behavior would help.
  • Cursor's second-opinion review (cursor-review.md) produced no output / was empty.
  • Codex's review aligned with the findings here: consensus-pubkey validation (reported as the inline blocker), the equal-power tie-break ordering, and the unenforced MaxVotingPowerRatio.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

commissionMaxRate := args[3].(string)
commissionMaxChangeRate := args[4].(string)
minSelfDelegation := args[5].(*big.Int)
pubKey, err := hex.DecodeString(pubKeyHex)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocker] createValidator hex-decodes pubKeyHex with no length/format validation, so an empty string or any wrong-length byte string is accepted as the consensus pubkey. That key is stored on the Validator and later emitted verbatim as precompiles.ValidatorUpdate{PubKey: ...} from endblock.go, which feeds consensus validator-set updates. The Cosmos path requires a valid cryptotypes.PubKey of a type supported by the consensus params; here an empty or malformed (e.g. non-32-byte ed25519) key produces an invalid validator update. Validate the decoded length against the expected consensus key type before accepting. (Also flagged by Codex.)

if left != right {
return left > right
}
return validators[i].OperatorAddress < validators[j].OperatorAddress

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Equal-power ties are broken by OperatorAddress in ascending order. Cosmos's power index orders tied validators by operator address in descending byte order (addresses are stored complemented in the ValidatorsByPower index). When tied candidates straddle the MaxValidators cutoff, this can select a different bonded set than the staking rules being replicated. Ordering is deterministic, but to match Cosmos consider reversing the tie-break (or document the intentional divergence). (Also flagged by Codex.)

} else if err := addPoolNotBonded(ctx.Store, useiAmount); err != nil {
return nil, err
}
p.emit(ctx, "Delegate", ctx.Caller, validatorAddress, util.CloneBig(ctx.ApparentValue))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The Delegate event amount is emitted in wei (ctx.ApparentValue), whereas Undelegate/Redelegate emit the usei amount. This unit inconsistency across the events is a footgun for log consumers — confirm it intentionally mirrors the legacy Sei staking precompile, and consider a comment noting it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant