feat(precompiles): add scoped vote authorization flow - #3846
Conversation
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3846 +/- ##
==========================================
- Coverage 61.54% 60.67% -0.87%
==========================================
Files 2361 2269 -92
Lines 199417 188996 -10421
==========================================
- Hits 122723 114681 -8042
+ Misses 65739 64194 -1545
+ Partials 10955 10121 -834
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
PR SummaryMedium Risk Overview New precompile entrypoints Wiring adds Reviewed by Cursor Bugbot for commit 29eb0ad. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
No bugs found, but this PR extends the governance EVM precompile to grant/exec/revoke Cosmos authz permissions (grantVoteAuthorization, voteWithAuthorization, revokeVoteAuthorization), which is a new permission-granting surface reachable from EVM contracts — worth a human look given the security sensitivity, even though the scope is deliberately narrowed to MsgVote.
What was reviewed:
- Confirmed the authz grant/exec/revoke path is hard-scoped to the
MsgVotetype URL (viaGenericAuthorization+ exact type-URL match), and that a granted vote authorization does not also enable weighted votes — verified in both the code andTestVoteAuthorizationFlow. - Checked expiration validation (
grantVoteAuthorizationrequires expiration strictly after block time) and the non-payable / staticcall / delegatecall guards, which reuse the same patterns as the existingvote/voteWeightedmethods. - Reviewed the keeper wiring in
app/precompiles.goandprecompiles/utils/expected_keepers.go— mechanical interface additions with no logic of their own.
Extended reasoning...
The PR adds three new governance precompile methods that wrap the native Cosmos authz message server (Grant/Exec/Revoke) to let EVM callers delegate and exercise MsgVote-only voting authorizations. The implementation deliberately hard-codes the authorization and execution path to MsgVote (never accepting arbitrary type URLs or message payloads), reuses the existing authz grant store, and adds end-to-end test coverage for grant, delegated vote, revoke, expiry, and the staticcall/delegatecall/non-payable rejection paths.
I did not find a logic bug in this diff — the type-URL scoping, expiration check, and guard clauses all look correct and are exercised by TestVoteAuthorizationFlow. That said, this PR is exposing a piece of the authz permission system (grant/exec/revoke) through the EVM precompile surface for the first time. That is exactly the kind of auth/permission-related, security-sensitive change that warrants a human reviewer's sign-off even in the absence of a concrete bug, particularly given the app-hash-breaking label and the fact that it enables new categories of on-chain delegated authority (governance votes cast by a grantee account) that did not previously exist on this surface.
There was a problem hiding this comment.
The scoped vote-authorization flow is implemented cleanly and idiomatically for this repo: it reuses the native authz msg server and grant store, hard-codes /cosmos.gov.v1beta1.MsgVote so no arbitrary message payloads are reachable, and correctly wires IsTransaction (which precompiles/setup_test.go enforces against the ABI). No blocking correctness or security issues found; the remaining notes are test-coverage gaps and small robustness/documentation improvements.
Findings: 0 blocking | 10 non-blocking | 5 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Cursor's second-opinion file (
cursor-review.md) is empty — that pass produced no output. Codex reported "No material issues found". - Interop is claimed but untested: no case covers a grant created directly through the native authz msg server (CLI-shaped
MsgGrant) then consumed viavoteWithAuthorization, nor a precompile-created grant being visible through the existing read-only authz precompile (grants/granteeGrants) and revocable by a nativeMsgRevoke. Those are the assertions that actually pin the "preserve Cosmos authz interoperability" goal. - No integration coverage added in
integration_test/precompile_tests/precompiles/gov.spec.ts, where the other gov write paths (vote,voteWeighted,deposit,submitProposal) are exercised end-to-end against a live chain. That spec builds its interface fromprecompiles/gov/abi.json, so the new methods are already reachable there. - Behavior worth documenting for integrators:
grantVoteAuthorizationusesGenericAuthorization, so the grantee can vote on any proposal with any option until expiry or revocation, and a new grant silently overwrites any pre-existingMsgVotegrant (including a narrower custom authorization created via Cosmos tooling). This is standard authzSaveGrantbehavior and requires the granter's own transaction, but it isn't stated in theGov.soldocstrings. - Verification note:
go build/go testcould not be run in this environment (command approval denied), so this review is static. The author reportsgo test ./precompiles/...,go test ./app, andgo test -race ./precompiles/govpassing. - 5 suggestion(s)/nit(s) flagged inline on specific lines.
| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
| expiration := time.Unix(args[1].(int64), 0).UTC() |
There was a problem hiding this comment.
[suggestion] expiration is unbounded above. A caller passing e.g. type(int64).max produces a time.Time outside the protobuf timestamp range (years 1..9999), so gogoproto's StdTimeMarshalTo errors inside authz.Keeper.SaveGrant → k.cdc.MustMarshal(&grant) panics. That panic is only caught by the blanket recover() at the top of Execute, and the caller sees a generic execution reverted: ....
State is still rolled back by the EVM snapshot so this isn't a correctness bug, but it makes the panic path load-bearing for ordinary user input. Consider bounding the value alongside the existing lower-bound check so callers get an explicit error:
if !expiration.After(ctx.BlockTime()) {
return nil, 0, errors.New("vote authorization expiration must be after the current block time")
}
if expiration.Year() > 9999 {
return nil, 0, errors.New("vote authorization expiration is out of range")
}| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
| voter, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) |
There was a problem hiding this comment.
[suggestion] GetSeiAddressFromArg errors with AssociationMissingErr when the argument address has no EVM↔Sei association, so voteWithAuthorization only works when the granter has associated an EVM address — likewise grantVoteAuthorization (line 210) requires the grantee to be associated. A Cosmos-native governance operator who granted MsgVote authority via the CLI therefore cannot be voted for through this precompile even though the grant exists on chain, which narrows the stated interop goal.
That's an inherent consequence of typing the parameter as address and is consistent with the rest of this precompile, so no change is required — but it's worth stating in the Gov.sol docstrings so integrators aren't surprised (or exposing a bech32-string variant the way staking does for validator addresses).
| false, | ||
| false, | ||
| granteeEVMAddr, | ||
| blockTime.Unix(), |
There was a problem hiding this comment.
[suggestion] This is the only "expiry" coverage, and it tests rejection at grant time (expiration == blockTime fails the !expiration.After(...) guard) rather than an actually expired grant. The PR description lists expiry as covered end-to-end.
Worth adding: grant with blockTime+1h, rebuild the context/statedb with a block time past the expiration, then assert voteWithAuthorization reverts and that GetCleanAuthorization prunes the stale grant. That exercises authz.Keeper.GetCleanAuthorization's grant.Expiration.Before(ctx.BlockHeader().Time) branch, which is the check that actually enforces expiry.
| return nil, 0, errors.New("vote authorization expiration must be after the current block time") | ||
| } | ||
|
|
||
| authorization := authztypes.NewGenericAuthorization(sdk.MsgTypeURL(&govtypes.MsgVote{})) |
There was a problem hiding this comment.
[nit] sdk.MsgTypeURL(&govtypes.MsgVote{}) is recomputed here and again in revokeVoteAuthorization (line 290). Hoisting it to a package-level var voteMsgTypeURL = sdk.MsgTypeURL(&govtypes.MsgVote{}) keeps the grant and revoke paths provably keyed on the same string and avoids the repeated reflection.
| /** | ||
| * @dev Grant an account permission to cast simple votes on behalf of the caller | ||
| * @param grantee The account receiving the vote authorization | ||
| * @param expiration Unix timestamp after which the authorization is invalid |
There was a problem hiding this comment.
[nit] The boundary semantics are slightly asymmetric and worth documenting: granting requires expiration strictly after the current block time, while an existing grant stays usable as long as expiration >= block time at execution. Also worth noting that there is no "never expires" option — Grant.Expiration is non-nullable in this fork, so every grant must carry a finite timestamp.
Summary
grantVoteAuthorization,voteWithAuthorization, andrevokeVoteAuthorizationto the governance precompileMsgVote, without accepting arbitrary message type URLs or Cosmos message payloadsWhy
Governance operators already use Cosmos authz to let another account vote on their behalf. Exposing the complete authz transaction API to EVM callers would create a much broader execution surface than this workflow needs. This change keeps the EVM API limited to simple governance votes while retaining compatibility with the existing authz state and execution model.
Validation
go test ./precompiles/...go test ./appgo test -race ./precompiles/govgo vet ./precompiles/gov ./precompiles/utils ./appgofmtandgoimportsgit diff --checkThe local golangci-lint v2.8 runner could not execute because its binary reports Go 1.24 and refuses the repository's Go 1.25.6 target.