Skip to content

feat(precompiles): add scoped vote authorization flow - #3846

Merged
codchen merged 1 commit into
mainfrom
codex/scoped-vote-authz-precompile
Aug 6, 2026
Merged

feat(precompiles): add scoped vote authorization flow#3846
codchen merged 1 commit into
mainfrom
codex/scoped-vote-authz-precompile

Conversation

@codchen

@codchen codchen commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add grantVoteAuthorization, voteWithAuthorization, and revokeVoteAuthorization to the governance precompile
  • hard-code the authorization and execution path to MsgVote, without accepting arbitrary message type URLs or Cosmos message payloads
  • preserve Cosmos authz interoperability by using the native authz message server and grant store
  • keep weighted-vote authorization outside this surface
  • add end-to-end coverage for grant, delegated vote, revoke, expiry, staticcall, delegatecall, and non-payable rejection

Why

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 ./app
  • go test -race ./precompiles/gov
  • go vet ./precompiles/gov ./precompiles/utils ./app
  • touched Go files pass gofmt and goimports
  • git diff --check

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 5, 2026, 4:11 AM

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.34783% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.67%. Comparing base (e34c457) to head (29eb0ad).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
precompiles/gov/gov.go 52.80% 21 Missing and 21 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
sei-chain-pr 54.43% <54.34%> (?)
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/gov/gov.go 65.00% <52.80%> (-2.23%) ⬇️

... and 92 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 a review from philipsu522 August 5, 2026 04:09
@codchen
codchen marked this pull request as ready for review August 5, 2026 04:10
@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes governance voting and authz grant state via the EVM precompile; scope is intentionally narrow (MsgVote only) but delegated voting affects on-chain tally outcomes.

Overview
Adds delegated simple voting on the governance EVM precompile so contracts and EOAs can use the same Cosmos authz grants as native users, without exposing a general-purpose authz API.

New precompile entrypoints grantVoteAuthorization, voteWithAuthorization, and revokeVoteAuthorization map to MsgGrant, MsgExec (wrapping MsgVote), and MsgRevoke. Grants are scoped to MsgVote only (generic authorization for that type URL)—not weighted votes and not arbitrary message types. Expiration must be after block time; calls reject payable value and follow existing gov precompile rules (no staticcall/delegatecall for txs).

Wiring adds AuthzMsgServer to precompile keepers (app/precompiles.go, precompiles/utils) and registers the new methods in Gov.sol, abi.json, and gov.go. TestVoteAuthorizationFlow covers grant → delegated vote → revoke and failure cases.

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

@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, 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 MsgVote type URL (via GenericAuthorization + exact type-URL match), and that a granted vote authorization does not also enable weighted votes — verified in both the code and TestVoteAuthorizationFlow.
  • Checked expiration validation (grantVoteAuthorization requires expiration strictly after block time) and the non-payable / staticcall / delegatecall guards, which reuse the same patterns as the existing vote/voteWeighted methods.
  • Reviewed the keeper wiring in app/precompiles.go and precompiles/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.

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

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 via voteWithAuthorization, nor a precompile-created grant being visible through the existing read-only authz precompile (grants / granteeGrants) and revocable by a native MsgRevoke. 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 from precompiles/gov/abi.json, so the new methods are already reachable there.
  • Behavior worth documenting for integrators: grantVoteAuthorization uses GenericAuthorization, so the grantee can vote on any proposal with any option until expiry or revocation, and a new grant silently overwrites any pre-existing MsgVote grant (including a narrower custom authorization created via Cosmos tooling). This is standard authz SaveGrant behavior and requires the granter's own transaction, but it isn't stated in the Gov.sol docstrings.
  • Verification note: go build / go test could not be run in this environment (command approval denied), so this review is static. The author reports go test ./precompiles/..., go test ./app, and go test -race ./precompiles/gov passing.
  • 5 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread precompiles/gov/gov.go
if err != nil {
return nil, 0, err
}
expiration := time.Unix(args[1].(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] 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.SaveGrantk.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")
}

Comment thread precompiles/gov/gov.go
if err != nil {
return nil, 0, err
}
voter, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper)

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] 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(),

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

Comment thread precompiles/gov/gov.go
return nil, 0, errors.New("vote authorization expiration must be after the current block time")
}

authorization := authztypes.NewGenericAuthorization(sdk.MsgTypeURL(&govtypes.MsgVote{}))

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

Comment thread precompiles/gov/Gov.sol
/**
* @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

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

@codchen
codchen added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit bca633e Aug 6, 2026
82 of 86 checks passed
@codchen
codchen deleted the codex/scoped-vote-authz-precompile branch August 6, 2026 03:32
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