Skip to content

feat(precompiles): expose module Msg rpcs as precompile methods - #3825

Open
codchen wants to merge 8 commits into
mainfrom
claude/precompile-msg-coverage
Open

feat(precompiles): expose module Msg rpcs as precompile methods#3825
codchen wants to merge 8 commits into
mainfrom
claude/precompile-msg-coverage

Conversation

@codchen

@codchen codchen commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes and provide context

Follow-up to #3767 (which exposed the missing Query rpcs): the same audit of service Msg rpc defs under /proto and /sei-cosmos/proto found several module Msgs with no EVM precompile counterpart. After review, this PR closes the one remaining applicable gap:

Precompile Method Msg rpc
slashing unjail() Unjail
  • unjail derives the validator operator from the caller's associated Sei address (like the distribution precompile's withdrawValidatorCommission) and calls the slashing msg server.
  • It rejects delegatecall (which would let a contract act on behalf of its own caller) and staticcall.
  • Plumbing follows the established shape: a narrow SlashingMsgServer interface in precompiles/utils/expected_keepers.go, wired to slashingkeeper.NewMsgServerImpl in app/precompiles.go.
  • No new precompile addresses, so no giga fail-fast or precompiles/setup.go changes. Like feat(precompiles): expose module Query rpcs as precompile methods #3767, this is a consensus-visible addition at the current head ABI version and should ride the next upgrade.

Reviewed and intentionally not implemented

  • bank MultiSend, authz Grant / Exec / Revoke, distribution FundCommunityPool, feegrant GrantAllowance / RevokeAllowance — descoped in review.
  • evm AssociateContractAddress — deprecated.
  • evidence SubmitEvidence — app.go never sets an evidence router, so the msg can never succeed on sei.
  • vesting CreateVestingAccount — the vesting module is deprecated (ErrVestingDeprecated).
  • oracle and tokenfactory are out of scope; gov and staking Msg services were already fully covered; epoch/mint/auth/params/upgrade define no Msg rpcs.

Testing performed to validate your change

  • Unit test covers the happy path against real keepers (a jailed validator with self-delegation and elapsed jail time is unjailed via the precompile) and rejections (staticcall, delegatecall, non-payable value, unassociated caller, caller not a validator operator).
  • go build ./..., go vet, gofmt -s / goimports clean; go test -race ./precompiles/... ./app passes locally.

🤖 Generated with Claude Code

Follow-up to #3767: add transaction methods for the module Msg rpcs that
had no EVM precompile counterpart — bank multiSend, distribution
fundCommunityPool, addr associateContractAddress, authz
grant/exec/revoke, feegrant grantAllowance/revokeAllowance, and slashing
unjail. evidence SubmitEvidence and vesting CreateVestingAccount are
intentionally omitted (no evidence router is set, and the vesting module
is deprecated); oracle and tokenfactory are out of scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 30, 2026, 3:59 PM

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.21053% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.28%. Comparing base (ccf96df) to head (da9e73c).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
precompiles/slashing/slashing.go 82.85% 3 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3825      +/-   ##
==========================================
- Coverage   60.24%   59.28%   -0.96%     
==========================================
  Files        2332     2240      -92     
  Lines      195184   184698   -10486     
==========================================
- Hits       117586   109498    -8088     
+ Misses      66953    65404    -1549     
+ Partials    10645     9796     -849     
Flag Coverage Δ
sei-chain-pr 52.34% <84.21%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?

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

Files with missing lines Coverage Δ
app/precompiles.go 100.00% <100.00%> (ø)
precompiles/utils/expected_keepers.go 100.00% <100.00%> (ø)
precompiles/slashing/slashing.go 79.06% <82.85%> (+0.49%) ⬆️

... and 96 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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codchen
codchen marked this pull request as ready for review July 30, 2026 03:29
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Consensus-visible validator state change on an existing precompile ABI; scope is narrow and follows established msg-precompile patterns with delegatecall/staticcall blocks.

Overview
Adds unjail() on the existing slashing precompile (0x…1014) so validators can unjail via EVM, wiring it to the Cosmos MsgUnjail path through new SlashingMsgServer plumbing in app/precompiles.go and precompiles/utils/expected_keepers.go.

The handler resolves the validator operator from the caller’s associated Sei address (same idea as distribution’s operator-only txs), rejects staticcall, delegatecall, and payable calls, and marks unjail as a state-changing method in IsTransaction. Solidity interface, abi.json, and TestUnjail cover success and failure cases.

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

seidroid[bot]
seidroid Bot previously requested changes Jul 30, 2026

@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.

Well-structured, consistent extension of five precompiles with real-keeper tests; the msg-server plumbing, delegatecall/staticcall guards, fundCommunityPool's payable pattern, and the re-implemented authz nested-EVM/depth ante checks all match existing conventions and the sei-cosmos APIs they call. One blocking issue: bank.multiSend is gated on the caller being the denom's native ERC20 pointer, but the only contract that can hold that role never calls it, so the method is unreachable on-chain; separately, authz.exec quietly exposes the entire cosmos msg router to the EVM, including modules this PR intentionally leaves out.

Findings: 2 blocking | 9 non-blocking | 6 posted inline

Blockers

  • bank.multiSend has no reachable caller on-chain (details in the inline comment on precompiles/bank/bank.go). Since this is a consensus-visible ABI addition, it should either ship together with the pointer-contract entry point that calls it, or the caller restriction/rationale should be reconsidered — as written the method can only be exercised from Go tests that fake the pointer address.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • Test gaps: (a) the maxNestedMsgs depth cap in checkNoNestedEVMMessages is never exercised (>5 nested MsgExec) — the test only covers one level of nesting with an EVM msg; (b) no test covers the exec path where the inner msg's signer is the caller (no grant needed), which is the widest new capability; (c) multiSend is only tested against a mock pointer address, so nothing validates it end-to-end from a pointer contract.
  • authz/feegrant/slashing reject delegatecall only for the new tx methods (checked after the query switch), while gov/distribution/pointer reject delegatecall for the whole precompile. Not a bug, but the inconsistency is worth a comment or normalizing.
  • In multiSend, total = total.Add(amount) uses sdk.Int, which panics if the sum exceeds 256 bits (reachable with two large uint256 amounts). The panic is caught by the Execute-level recover() and surfaces as execution reverted, so it's safe, but an explicit overflow check would give a clearer error and not consume all gas.
  • Second-opinion passes: Codex reported "No material issues found"; cursor-review.md is empty, i.e. the Cursor pass produced no output — its coverage should not be assumed.
  • 5 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread precompiles/bank/bank.go Outdated
if denom == "" {
return nil, 0, errors.New("invalid denom")
}
pointer, _, exists := p.evmKeeper.GetERC20NativePointer(ctx, denom)

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] multiSend looks unreachable on-chain as shipped. The gate requires caller == GetERC20NativePointer(denom), and a native ERC20 pointer address is only ever set by UpsertERCNativePointer (via the pointer precompile's addNativePointer, HandleAddERCNativePointerProposalV2, and pointer_upgrade.go), which always deploys the chain's own NativeSeiTokensERC20 artifact. That contract only ever calls BankPrecompile.send(...) in _update — there is no batch-transfer entry point, and contracts/src/precompiles/IBank.sol wasn't extended either. So no address that can satisfy this check has code that calls multiSend; the only caller in existence is TestMultiSend, which registers a MockAddressPair EOA as the pointer.

If the intent is to land the precompile side first and add the pointer-contract path (plus a new native artifact version) in a follow-up, please say so explicitly — otherwise this adds a consensus-visible ABI method that can never be invoked.

Comment thread precompiles/authz/authz.go Outdated
// authz grantee. Messages whose signer is not the caller require a matching
// grant. EVM messages are rejected, mirroring the cosmos ante handler's
// restriction on nested EVM messages in MsgExec.
func (p PrecompileExecutor) exec(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) ([]byte, uint64, error) {

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] exec is materially broader than the other methods in this PR. Keeper.DispatchActions implicitly accepts any message whose single signer equals the grantee, so with no grant at all a caller can dispatch any message registered on the MsgServiceRouter from their own Sei address, inside an EVM call frame. That includes exactly the msgs this PR deliberately excludes — oracle and tokenfactory ("out of scope"), vesting CreateVestingAccount, evidence SubmitEvidence — plus x/evm's own MsgSend, MsgRegisterPointer, and MsgAssociate*. Only two ante decorators are re-implemented here (nested-EVM rejection and depth cap); everything else in app/ante.go is bypassed, and per-module precompile guards (e.g. wasmd's pointer/delegatecall checks) are sidestepped for the same operations.

The state transitions are already reachable by the same actors via a cosmos MsgExec tx, so I don't think this is privilege escalation — but making the entire msg router EVM-reachable is a much bigger surface decision than "expose the authz Msg rpcs," and it silently undoes this PR's own scoping choices. Consider an explicit allow/deny list of dispatchable msg type URLs (or at minimum documenting the decision in the PR/Authz.sol).

Comment thread precompiles/authz/authz.go Outdated
msgs[i] = m
}

if err := checkNoNestedEVMMessages(msgs, 0); err != nil {

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] MsgExec.ValidateBasic() in sei-cosmos only checks the grantee address and that Msgs is non-empty — it does not validate the inner messages. So attacker-supplied inner msgs go straight to DispatchActions, whose first step is msg.GetSigners(); most msg types (banktypes.MsgSend, authz.MsgGrant, …) panic there on a malformed bech32 address. It only becomes a revert because of the recover() at the top of Execute, and that path returns remainingGas = 0, so the caller is charged all supplied gas for a plain input error. Calling m.ValidateBasic() on each parsed msg in the loop above (alongside the nested-EVM scan) would turn these into clean, cheap errors.

Comment thread precompiles/authz/authz.go Outdated
return nil, 0, fmt.Errorf("failed to parse authorization JSON: %w", err)
}

expiration := time.Unix(args[2].(int64), 0).UTC()

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] Nothing validates that expiration is in the future, and neither MsgGrant.ValidateBasic nor Grant.ValidateBasic checks it in this SDK version (the block-time check in authz.NewGrant is commented out). So grant(grantee, auth, 0) — the natural encoding a caller would reach for to mean "no expiration" — succeeds and stores a grant dated 1970 that GetCleanAuthorization deletes on first use, i.e. a silently dead grant that still shows up in granterGrants. Consider rejecting non-future timestamps here, and documenting in Authz.sol that there is no "never expires" value.

event DelegationRewardsWithdrawn(address indexed delegator, string validator, uint256 amount);
event MultipleDelegationRewardsWithdrawn(address indexed delegator, string[] validators, uint256[] amounts);
event ValidatorCommissionWithdrawn(string indexed validator, uint256 amount);
event CommunityPoolFunded(address indexed depositor, uint256 amount);

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] Worth documenting that amount is in usei, not wei: the caller supplies msg.value in wei, but the event carries coin.Amount (usei) from HandlePaymentUsei. Consumers indexing this event will otherwise reasonably assume it mirrors msg.value.

"github.com/sei-protocol/sei-chain/app"
"github.com/sei-protocol/sei-chain/precompiles/slashing"
"github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/ed25519"
crptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types"

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] Typo in the import alias: crptotypescryptotypes (used again on the setupValidator signature below).

@claude claude 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.

No bugs found by the automated review, but this is a large, app-hash-breaking PR that adds new fund-moving and permission-granting entry points (authz grant/exec/revoke, bank multiSend, distribution fundCommunityPool, feegrant grant/revokeAllowance, slashing unjail) across five precompiles, so it's worth a human look before merge.

What was reviewed: authz exec's nested-EVM-message rejection and depth-5 cap were checked line-by-line against app/antedecorators.AuthzNestedMessageDecorator and match its recursion and cap exactly. multiSend's input/output construction (single Input summing to the total, per-output zero-amount no-ops) was checked for balance-conservation and mirrors send's existing pointer-only caller restriction. fundCommunityPool's payment handling (HandlePaymentUsei refund-then-debit) was compared against the existing gov deposit payable flow and follows the same established pattern. Staticcall/delegatecall guards are present on all new authz/feegrant/slashing methods; bank's new multiSend only guards staticcall, consistent with (not weaker than) the existing send method it mirrors.

Extended reasoning...

This PR adds five new state-mutating precompile methods (authz grant/exec/revoke, bank multiSend, distribution fundCommunityPool, feegrant grantAllowance/revokeAllowance, slashing unjail) that let EVM contracts move funds, grant permissions, and execute messages on behalf of other accounts. It carries the app-hash-breaking label and touches money-movement and authorization logic in five modules simultaneously (authz, bank, distribution, feegrant, slashing), plus the shared keeper wiring in app/precompiles.go and precompiles/utils/expected_keepers.go.

The main security-relevant surface is authz's exec, which can spend a granter's funds on behalf of a grantee once a grant exists (this is authz's intended behavior, not a bug) and which re-implements the ante handler's nested-EVM-message rejection since precompile calls bypass ante decorators; I compared that reimplementation against app/antedecorators/authz_nested_message.go and found it to be a faithful port (same recursion structure, same depth-5 cap). All new caller-identity methods that act 'on behalf of' the caller correctly reject delegatecall and staticcall except bank's multiSend, which only rejects staticcall — but this matches the existing send method's guard level, not a regression.

Given the combination of the app-hash-breaking label, the number of modules touched in one PR, and the security-sensitive nature of authorization/fund-movement precompiles, I believe this exceeds the bar for auto-approval even with no automated findings; a human with authz/bank/distribution domain context should confirm the money-movement and permission semantics are exactly as intended before this rides the next upgrade.

Test coverage is solid (real-keeper round-trip tests for grant/exec/revoke, community-pool balance+event assertions via signed EVM tx, jailed-validator unjail, and negative tests for staticcall/delegatecall/unassociated-caller/malformed-JSON/nested-EVM-message), which lowers but doesn't eliminate the need for a human pass given the blast radius of a mistake in this code.

codchen and others added 2 commits July 30, 2026 12:04
Drop the ERC20-pointer gating: multiSend no longer requires the denom to
have a native pointer nor the caller to be it. The caller can only spend
its own funds (association required), so delegatecall is rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codchen
codchen force-pushed the claude/precompile-msg-coverage branch from 06c9bc2 to a849a0a Compare July 30, 2026 04:04

@claude claude 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.

No bugs found in this run. This PR adds four new consensus-visible, fund/state-moving precompile methods (bank multiSend, distribution fundCommunityPool, feegrant grantAllowance/revokeAllowance, slashing unjail) touching real money movement and validator state, so I think it's worth a human look before merging even without findings.

What was reviewed: the multiSend caller-authentication path (confirmed it now derives from the caller's associated Sei address rather than the pointer-gate that an earlier review round flagged as unreachable), the fundCommunityPool payable/usei-only flow and its precompile-wide delegatecall guard, the feegrant grant/revoke protobuf-JSON allowance parsing and msg-server wiring, and the slashing unjail caller-to-validator-operator derivation.

Extended reasoning...

This PR (app-hash-breaking) adds four new state-changing precompile methods across bank, distribution, feegrant, and slashing that mirror existing Cosmos SDK Msg handlers. Security-sensitive surface includes: multiSend batch-spending the caller's own funds (guarded by associated-Sei-address check + delegatecall/staticcall rejection, not pointer-gated), fundCommunityPool moving usei via the shared HandlePaymentUsei payable pattern, feegrant grant/revoke allowances via protobuf-JSON-encoded Any parsing, and slashing unjail deriving the validator operator from the caller's associated address.

An earlier review round on this PR flagged multiSend as unreachable on-chain because it was gated on the ERC20 native pointer address (which has no batch-send caller). The current diff no longer has that gate — it authenticates via the caller's associated Sei address instead, matching the PR description's stated intent ("not gated on pointer contracts"), so that specific concern appears resolved in this revision. Other prior review comments on this thread (authz exec/grant validation, expiration checks) reference precompiles/authz/authz.go, which is not part of this PR's diff (18 changed files, no authz.go) — those appear to be stale from a different revision/PR and don't apply to what's currently proposed here.

Level of scrutiny: this warrants careful human review despite the bug hunter finding nothing, because it's labeled app-hash-breaking, moves real funds (multiSend, community pool funding), and changes validator jail state — mistakes here have direct financial/consensus impact for a live chain, which is a different risk profile than incorrect view/query methods.

seidroid[bot]
seidroid Bot previously requested changes Jul 30, 2026

@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.

Well-structured extension of four precompiles with sound caller-identity guards (delegatecall/staticcall rejection, association requirements) and good happy-path + rejection test coverage. One blocker: fundCommunityPool makes the distribution precompile payable but distribution.DistrAddress was never added to the payablePrecompiles allowlist, so every call will emit a spurious, unbalanced cosmos transfer event to 0x…1007.

Findings: 2 blocking | 8 non-blocking | 4 posted inline

Blockers

  • fundCommunityPool makes the distribution precompile payable, but distribution.DistrAddress (0x0000000000000000000000000000000000001007) is not added to the payablePrecompiles map in x/evm/keeper/precompile.go or its giga mirror giga/deps/xevm/keeper/precompile.go. Both files carry the comment "add any payable precompiles here / these will suppress transfer events to/from the precompile address", and every other payable precompile (bank, staking, gov, wasmd) is registered. Without the entry, k.GetVMBlockContext's txfer hook takes the core.Transfer branch instead of state.TransferWithoutEvents, so DBImpl.SubBalance/AddBalance run with eventsSuppressed == false and emit coin_spent/coin_received/transfer cosmos events for the caller→precompile leg. pcommon.HandlePaymentUsei then refunds the depositor on a fresh, discarded event manager (ctx.WithEventManager(sdk.NewEventManager())), so the return leg emits nothing. Net result: every fundCommunityPool tx leaves a spurious, unbalanced transfer event showing usei moving into the distribution precompile address and never coming back — event-derived balance tracking in indexers/explorers will drift. Fix by adding distribution.DistrAddress: {} to both maps.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • The Cursor second-opinion pass produced no outputcursor-review.md is empty, so that pass contributed nothing to this synthesis.
  • Both Codex findings are stale and should be disregarded. Codex P1 ("missing authz implementation") and P2 ("multiSend omits the documented pointer restriction") both describe the first commit's message rather than the final diff. The net PR diff touches only bank, distribution, feegrant, slashing, precompiles/utils/expected_keepers.go and app/precompiles.go — no authz/addr changes remain — and the PR description explicitly descopes authz and states that multiSend is intentionally not pointer-gated. The design rationale holds on review: unlike send, multiSend derives the sender from the caller's own association rather than an arbitrary fromAddress argument, and it rejects delegatecall, so the pointer gate is not load-bearing here.
  • Test coverage gaps. TestFundCommunityPool does not exercise the delegatecall rejection that the PR description claims for all new caller-identity methods (the guard is inherited from distribution's Execute, so it works, but it is untested for this method). TestMultiSend covers only a tokenfactory-style denom (ufoo) — no case for the base denom (usei) or for an output addressed to a blocked module account (BlockedAddr in msgServer.MultiSend).
  • TestFundCommunityPool's ordering dependency is a latent maintenance hazard. As the PR notes, the test must sit last in distribution_test.go because funding the pool grows the FeePool and shifts later tests' exact gas assertions. A future test appended after it will silently break unrelated assertions with a confusing failure. Consider a t.Cleanup that restores the pool, using an isolated app instance for this test, or at minimum a comment at the top of the file warning against appending after it.
  • Nit: the new crptotypes import alias in precompiles/slashing/slashing_test.go is a typo for cryptotypes (copied from the pre-existing one in distribution_test.go).
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

return
}

coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), depositor, value, p.bankKeeper, p.evmKeeper, hooks, evm.GetDepth())

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] This makes the distribution precompile payable for the first time, but distribution.DistrAddress was not added to the payablePrecompiles allowlist in x/evm/keeper/precompile.go (or the giga mirror giga/deps/xevm/keeper/precompile.go).

That map drives txfer in Keeper.GetVMBlockContext: registered addresses get state.TransferWithoutEvents (which sets eventsSuppressed, so DBImpl.SubBalance/AddBalance swap in a throwaway event manager), unregistered ones get plain core.Transfer. Because 0x…1007 is unregistered, the EVM's caller→precompile value transfer will emit real coin_spent/coin_received/transfer cosmos events, while the HandlePaymentUsei refund on this line runs on ctx.WithEventManager(sdk.NewEventManager()) and emits nothing.

So each fundCommunityPool call leaves a one-sided transfer event moving usei into the distribution precompile address with no offsetting event — the exact inconsistency the allowlist exists to prevent (bank, staking, gov and wasmd are all listed for their own payable methods).

Please add distribution.DistrAddress: {} to payablePrecompiles in both files.

Comment thread precompiles/bank/bank.go Outdated
return nil, 0, err
}
denom := args[1].(string)
if denom == "" {

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] multiSend accepts any denom, including the base denom (usei). Because it hands off straight to bankMsgServer.MultiSend, a usei transfer here bypasses DBImpl entirely: no OnBalanceChange, and none of the hooks.OnEnter/OnExit legs that sendNative emits (lines 372–384) to make native value movement visible to tracers. debug_traceTransaction will therefore show EVM-visible balances changing with nothing in the trace to account for it.

send avoids this in practice only incidentally — its pointer gate means a denom needs a registered ERC20 native pointer to be movable. multiSend drops that gate, so this becomes reachable for the base denom.

Suggest either rejecting denom == p.evmKeeper.GetBaseDenom(ctx) here (pointing callers at sendNative), or emitting the same tracing hooks sendNative does.

Comment thread precompiles/bank/bank.go Outdated
}
toAddresses := args[0].([]common.Address)
amounts := args[2].([]*big.Int)
if len(toAddresses) == 0 || len(toAddresses) != len(amounts) {

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] Slightly inconsistent handling of no-op calls: multiSend([], denom, []) reverts with "must be non-empty", but multiSend([a], denom, [0]) falls through the zero-amount continue to the len(outputs) == 0 short circuit at line 306 and returns true having done nothing. Worth either dropping the len(toAddresses) == 0 check (letting both hit the short circuit) or documenting the asymmetry in Bank.sol.

event DelegationRewardsWithdrawn(address indexed delegator, string validator, uint256 amount);
event MultipleDelegationRewardsWithdrawn(address indexed delegator, string[] validators, uint256[] amounts);
event ValidatorCommissionWithdrawn(string indexed validator, uint256 amount);
event CommunityPoolFunded(address indexed depositor, uint256 amount);

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] amount here is in usei (coin.Amount), while the caller supplies the deposit as msg.value in wei — a 10^12 discrepancy an integrator comparing the two would hit. This matches the existing ValidatorCommissionWithdrawn convention, so no need to change the unit, but the natspec on fundCommunityPool documents the wei-remainder rule and not the event's unit. Worth an explicit /// @dev amount is denominated in usei on the event.

Comment thread precompiles/distribution/distribution.go Outdated
Comment thread precompiles/bank/bank.go Outdated
codchen and others added 2 commits July 30, 2026 12:35
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fundCommunityPool is the first payable method on the distribution
precompile, so distribution.DistrAddress must be in payablePrecompiles
(both the x/evm and giga copies): otherwise the EVM value transfer into
the precompile emits a coin_received event for the precompile address
with no offsetting event, since the refund inside HandlePaymentUsei runs
on a discarded event manager. Also assert the absence of that event in
TestFundCommunityPool; the assertion fails without the allowlist entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@claude claude 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.

No new bugs found this run, but this PR remains worth a human look given its history and scope: it adds real state-moving precompile methods (community-pool funding, fee-grant allowances, validator unjail) at the current head ABI version and is marked app-hash-breaking.

What was reviewed:

  • Confirmed the payable-allowlist gap from earlier review rounds is now fixed: distribution.DistrAddress is present in payablePrecompiles in both x/evm/keeper/precompile.go and the giga mirror.
  • Confirmed multiSend (bank) and exec/grant (authz), both flagged as broken/unreachable in earlier rounds, were removed entirely rather than patched — verified neither exists in the current precompiles/bank/bank.go or precompiles/authz/authz.go.
  • Checked the feegrant grantAllowance zero-expiration candidate this run's bug hunt raised — ruled out (matches upstream authz/feegrant validation, which doesn't check expiration either; not a regression introduced here).
Extended reasoning...

This run found no new bugs. The three concrete issues raised by reviewers across this PR'''s earlier revisions have all been resolved as of the current head (commit 350b1da): the missing payablePrecompiles entry for distribution was added, and the unreachable/overbroad multiSend/authz exec methods were dropped from scope entirely rather than patched (per the PR description'''s explicit descoping and the Drop bank multiSend/Drop authz msg methods commits).

Despite the clean state, I'''m deferring rather than approving because the remaining surface is still consensus-critical: fundCommunityPool moves real value through a newly-payable precompile, grantAllowance/revokeAllowance create/destroy fee-spending authority for other addresses, and unjail lets a caller submit MsgUnjail on a validator'''s behalf. The PR is tagged app-hash-breaking, meaning these additions change consensus-visible behavior at the current ABI version. Given the history of real, non-trivial bugs found and fixed across this PR'''s life (the payable-allowlist gap, the unreachable/overbroad methods), I think a human should do a final pass before merge even though the current diff itself looks correct.

@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 fundCommunityPool (payable), feegrant grantAllowance/revokeAllowance, and slashing unjail to the head precompile versions; the payment flow, delegatecall/staticcall/association guards, ABI↔Solidity consistency, and the mirrored payablePrecompiles change all check out. No blockers — only documentation, test-coverage, and test-ordering-fragility notes.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion passes: cursor-review.md is empty (that pass produced no output); codex-review.md reported no material issues. My independent pass also found no blockers.
  • Missing negative-path tests for fundCommunityPool: (a) a value with a non-zero wei remainder (not a multiple of 10^12) should be rejected by HandlePaymentUsei, and (b) delegatecall rejection. The delegatecall guard for distribution lives in the shared Execute prologue rather than in the method, so a future refactor that moves it would not be caught by any distribution test.
  • TestUnjail/setupValidator mutate the shared testkeeper.EVMTestApp (create + jail a validator, and force UpdateStatus(Unbonded) via SetValidator without refreshing the power index). Harmless today since it is the last test in the only file in that package, but it carries the same ordering fragility as TestFundCommunityPool.
  • Consider also asserting the new methods in a shared "all tx methods reject delegatecall/staticcall" table test rather than per-method ad-hoc calls, so future precompile methods can't silently skip the guards.
  • 5 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread x/evm/keeper/precompile.go Outdated
staking.StakingAddress: {},
gov.GovAddress: {},
wasmd.WasmdAddress: {},
distribution.DistrAddress: {},

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] Making the distr precompile payable is app-hash-affecting, but TestIsPayablePrecompile (in x/evm/keeper/precompile_test.go and the giga mirror giga/deps/xevm/keeper/precompile_test.go) still only asserts bank/staking/gov. Please add require.True(t, evmkeeper.IsPayablePrecompile(toAddr(distribution.DistrAddress))) to both so an accidental removal of this entry is caught by unit tests rather than only by TestFundCommunityPool's event assertion.

event DelegationRewardsWithdrawn(address indexed delegator, string validator, uint256 amount);
event MultipleDelegationRewardsWithdrawn(address indexed delegator, string[] validators, uint256[] amounts);
event ValidatorCommissionWithdrawn(string indexed validator, uint256 amount);
event CommunityPoolFunded(address indexed depositor, uint256 amount);

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] amount here is the usei amount (the coin.Amount packed at distribution.go:862), not the wei msg.value the caller sent — i.e. sending 100_000_000_000_000 wei emits amount = 100. That matches the other events on this interface, but since this is the first payable method it's easy for an EVM consumer to misread. Worth a natspec line on the event (or on fundCommunityPool) stating the event amount is denominated in usei.

Comment thread precompiles/feegrant/Feegrant.sol Outdated
// allowance is protobuf-JSON encoded (the same encoding the queries
// return), e.g.
// {"@type":"/cosmos.feegrant.v1beta1.BasicAllowance","spend_limit":[{"denom":"usei","amount":"1000"}],"expiration":null}
function grantAllowance(address grantee, bytes memory allowance) external returns (bool success);

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] grantAllowance resolves the grantee via pcommon.GetSeiAddressFromArg, which errors with AssociationMissing for an EVM address that has no Sei association (and for the zero address). Since a fee grant is most useful for a new account that hasn't transacted yet, this precondition is likely to surprise callers — worth documenting here (and on revokeAllowance) that the grantee must already have an associated Sei address.

return
}

if err := p.distrKeeper.FundCommunityPool(ctx, sdk.NewCoins(coin), depositor); err != nil {

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] Unlike the feegrant/slashing methods in this PR (which route through the module msg servers), this calls the keeper directly, so no MsgFundCommunityPool typed/message cosmos event is emitted — only the underlying bank transfer events. That's consistent with the rest of this precompile, so not a blocker, but given the PR's framing as "expose module Msg rpcs", routing through distrtypes msg server would keep indexer output uniform with the other new methods.

}
}

func TestFundCommunityPool(t *testing.T) {

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] The PR description notes this test must stay at the end of the file because it grows the shared EVMTestApp FeePool and earlier tests assert exact gas values. That constraint isn't recorded anywhere in the code — please add a short comment here so a future reader doesn't move the function or insert a new test after it and get a confusing gas-assertion failure elsewhere in the file.

@seidroid
seidroid Bot dismissed stale reviews from themself July 30, 2026 04:52

Superseded: latest AI review found no blocking issues.

codchen and others added 2 commits July 30, 2026 23:55
Also reverts the distr entry in payablePrecompiles, which existed only
for fundCommunityPool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@claude claude 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.

LGTM — this PR has been narrowed down to just the slashing unjail() precompile method; the earlier bank/authz/distribution/feegrant additions that drew the blocker/suggestion comments above were all dropped in later commits (a849a0a, 8a81e7c, 68d3043, da9e73c), so those findings no longer apply to the current diff.

What was reviewed: the unjail flow (caller → Sei address → sdk.ValAddressMsgUnjail → slashing msg server), the delegatecall/staticcall/non-payable/unassociated-caller/non-operator rejection paths, and the new SlashingMsgServer plumbing in app/precompiles.go and precompiles/utils/expected_keepers.go. This follows the same caller-derived-operator pattern as distribution's existing withdrawValidatorCommission, and test coverage exercises both the happy path and each rejection case.

Extended reasoning...

Overview

This PR now only adds a single new precompile transaction method: slashing unjail(), which lets a validator operator unjail themselves via the EVM by deriving their validator address from their associated Sei address and calling MsgUnjail through a new narrow SlashingMsgServer interface. Wiring is added to app/precompiles.go and precompiles/utils/expected_keepers.go following the existing keeper-interface pattern used for other msg-server-backed precompile methods. The PR previously included several additional precompile methods (bank multiSend, authz grant/exec/revoke, distribution fundCommunityPool, feegrant grantAllowance/revokeAllowance) which drew multiple blocker/suggestion comments from Cursor bugbot, seidroid bugbot, and my own prior review — all of that code has since been removed in commits a849a0a, 8a81e7c, 68d3043, and da9e73c, and the current diff (confirmed via the PR API) touches only the 6 slashing/precompile-plumbing files reviewed here.

Security risks

unjail acts on behalf of the caller only (derived from their own EVM→Sei association, never a passed-in argument), and rejects both delegatecall (which would otherwise let a contract act on behalf of its own caller) and staticcall. The underlying MsgUnjail handler enforces that the caller is actually the validator's operator and applies the module's own jailing/self-delegation preconditions, so there's no new privilege beyond what the existing Cosmos msg already allows. No funds movement, no new precompile address, no changes to payability.

Level of scrutiny

This is a narrow, additive change following a well-established pattern in this codebase (same shape as distribution's withdrawValidatorCommission), touching non-critical-path code (an opt-in precompile method), with unit tests covering the happy path and every rejection branch (staticcall, delegatecall, non-payable, unassociated caller, non-operator caller). Light-to-moderate scrutiny is appropriate, and I found nothing beyond what's already covered by tests.

Other factors

The PR is flagged app-hash-breaking since it's a consensus-visible ABI addition, but the author's description notes this is expected to ride the next upgrade, consistent with how #3767 (the referenced prior PR) was handled. No CODEOWNERS are currently enforced on the touched paths. The prior review threads (mine and the two bots') were entirely about code that is no longer part of this PR, so none of those concerns carry over to the current state.

@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 well-scoped unjail() tx method to the slashing precompile; authorization (operator derived from the caller's associated Sei address), delegatecall/staticcall/non-payable guards, IsTransaction classification, keeper plumbing and versioning all follow the established distribution/gov patterns and look correct. Only nits and consistency notes — no blockers.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • cursor-review.md is empty — the Cursor pass produced no output, so this review is Claude + Codex only.
  • Codex's only finding (P2: "PR description is stale, claims distribution and feegrant methods/tests are included") does not hold against the current description, which lists only slashing unjail() and explicitly descopes distribution FundCommunityPool and feegrant Grant/RevokeAllowance. Likely written against an earlier revision; no action needed.
  • unjail emits no EVM log, so contracts/indexers can't observe it from receipts (distribution emits ValidatorCommissionWithdrawn etc. for its tx methods, though gov's vote also emits nothing). Optional, and adding one later is itself a consensus-visible change, so if it's wanted it's cheaper to add now in the same ABI version.
  • There is no e2e coverage for the slashing precompile in contracts/test/ — consistent with the current state (slashing has none at all), just noting unjail() is unit-test-only.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

// Transaction methods act on behalf of the caller, so they must not be
// reachable through delegatecall (which would let a contract act on
// behalf of its own caller) or staticcall.
if ctx.EVMPrecompileCalledFromDelegateCall() {

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] Worth an explicit note that placing the delegatecall guard after the query dispatch is deliberate. gov, distribution, and staking all reject delegatecall at the top of Execute for every method including views; here the existing params/signingInfo/signingInfos views stay delegatecall-reachable. That's the right call (tightening them would be a consensus-visible behavior change for existing contracts), but the current comment only explains why tx methods are guarded, not why the guard sits below the view switch — a future reader could easily "fix" the ordering to match gov and silently break callers.

case UnjailMethod:
return p.unjail(ctx, method, caller, args, value)
}
return

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] This naked return yields (nil, 0, nil) — reported success, empty output, and remainingGas == 0 so the caller's entire gas is consumed. Pre-existing shape (gov/distribution do the same), but now that Execute has two dispatch switches it's easier for a future method added to abi.json to land here unhandled. Returning an explicit fmt.Errorf("unknown method %s", method.Name) would fail loudly instead.

"github.com/sei-protocol/sei-chain/app"
"github.com/sei-protocol/sei-chain/precompiles/slashing"
"github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/ed25519"
crptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types"

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] Typo in the import alias: crptotypescryptotypes (the alias used elsewhere in the repo).

require.Equal(t, vm.ErrExecutionReverted, err)
}

func setupValidator(t *testing.T, ctx sdk.Context, a *app.App, bondStatus stakingtypes.BondStatus, valPub crptotypes.PubKey) sdk.ValAddress {

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] bondStatus is only ever passed stakingtypes.Unbonded, and since CreateValidatorMsg/Handle already leaves the validator Unbonded until the staking EndBlocker runs, val.UpdateStatus(bondStatus) + SetValidator are effectively no-ops. Consider dropping the parameter (and the redundant status write) until a second caller actually needs 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.

2 participants