Skip to content

Remove the EVM pointer creation path - #4230

Open
alexander-sei wants to merge 7 commits into
mainfrom
remove-erc-pointer-creation
Open

alexander-sei wants to merge 7 commits into
mainfrom
remove-erc-pointer-creation

Conversation

@alexander-sei

@alexander-sei alexander-sei commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

An EVM pointer is an ERC20 standing in for a native bank denom, or an ERC20/721/1155 standing in for a CW20/CW721/CW1155 contract. Three routes could create one: the pointer precompile at 0x…100b, the AddERCNativePointerProposalV2 governance proposal, and the seid tx evm register-evm-pointer and add-erc-native-pointer commands driving them. #4198 closed the opposite direction, CosmWasm wrappers for ERC tokens; this closes the remaining one, so no pointer of any kind can be created from here.

The precompile keeps its address and ABI, and every call now returns ErrPointerPrecompileRetired with an Error(string) payload, following precompiles/ibc and precompiles/oracle. Unregistering 0x…100b was the alternative and is worse: a call to an address with no code succeeds and returns nothing, so IPointer(…).addNativePointer(…) would start failing silently instead of reverting. x/evm/gov.go, NewProposalHandler and the two CLI commands are gone, and migration boundaries 8 and 17 join 4, 6, 7, 9 and 15 as inert RetiredPointerBoundary entries, since a module's migration sequence has to stay unbroken for a chain upgrading across it.

The eight Add*Pointer*Proposal types are retired rather than deleted, which is the part worth scrutinising. Gov never prunes a proposal once its vote ends, MustUnmarshalProposal unpacks the content Any on every read, and Router.GetRoute panics on a route it cannot resolve — so dropping the registration would panic a node reading an old proposal, and dropping the evm route would panic one still in the voting queue. Both stay, and ValidateBasic, ValidateProposalSubmission and the route handler all refuse with ErrPointerProposalDeprecated. The giga/deps/xevm copies were never wired into gov and are deleted outright. Keeper.UpsertERCPointer and the bytecode under x/evm/artifacts/ stay, because precompiles/pointer/legacy/v66 still needs them to replay historical creation under debug_trace*.

The Solidity side goes with the capability. Pointer.sol, IBC.sol and Oracle.sol are the published interfaces of the three precompiles that now refuse every call, so a contract author cannot compile against an API that only reverts. The four pointer sources under contracts/src go too: the Go build embeds the checked-in .abi and .bin and never reads a .sol, and the bytecode is frozen, so a rebuild could only produce something that differs from what is deployed. That takes their three Foundry tests, which import them, and orphans the four interfaces under contracts/src/precompiles — and since those were the repo's only .t.sol files, forge-test.yml would have tested an empty project and is removed. foundry.toml and contracts/lib stay, because five remaining sources resolve @openzeppelin through them. x/evm/artifacts/README described regenerating the artifacts from sources that no longer exist and now records that they are frozen instead.

Consensus at current height is unaffected and the read path is untouched: registry and reverse-registry lookups, the pointerview precompile, and the pointer, pointer-version and pointee queries all keep serving pointers already deployed on chain. Three evmrpc/tests regression fixtures move, because #4213 removed the pre-v6.6 snapshots and pacific-1 pointer-creation traces below the v6.6 upgrade height now resolve to the live precompile and report the retirement rather than the deploy, one of them flipping from success to revert. Integration coverage that created pointers goes as well: the four ERC*Pointer hardhat suites and the two integration-test-matrix.json rows that ran them, the pointer-backed dapp and rpc scenarios, and pointerview.spec.ts's registered-pointer case, which leaves that spec covering unregistered lookups only. Validated with go build, go vet, make fmtcheck, golangci-lint run, tsc --noEmit on both TypeScript suites, and scripts/ramtest.sh over x/evm, precompiles, app, evmrpc, occ_tests, giga, wasmbinding and testutil.

alexander-sei and others added 2 commits September 17, 2026 11:55
An EVM pointer is an ERC20 for a native denom, or an ERC20/721/1155 for
a CW20/CW721/CW1155 contract. Three routes could create one: the pointer
precompile at 0x...100b, the AddERCNativePointerProposalV2 governance
proposal, and the seid commands driving them. All three are closed here.

The precompile keeps its address and ABI so the four add* selectors
still decode and revert with an Error(string) reason, following the
retirement already applied to the ibc and oracle precompiles.
Unregistering the address would instead let a call succeed against empty
code and return nothing, failing a caller silently rather than loudly.

All eight pointer governance proposals refuse submission and execution
but stay registered as decode-only content. Gov never prunes a proposal
once its vote ends, MustUnmarshalProposal unpacks the content Any on
every read, and Router.GetRoute panics on a route it cannot resolve, so
dropping either the registration or the evm route would fault a node
reading or tallying an old proposal. The evm route now points at a
handler that fails, mirroring app/retiredibc/gov. The giga/deps/xevm
copies were never wired into gov and are deleted outright.

Migration boundaries 8 and 17 join 4, 6, 7, 9 and 15 as inert: their only
body was pointer creation, and a module's migration sequence must be
unbroken for a chain that upgrades across it.

Keeper.UpsertERCPointer and the pointer bytecode under artifacts/ are
retained because precompiles/pointer/legacy/* replays historical creation
under debug_trace*. Nothing live reaches them. The read side is
untouched: registry and reverse-registry lookups, the pointerview
precompile, and the pointer, pointer-version and pointee queries all
keep serving pointers already deployed on chain.

No integration test can create a pointer any more, so the four
ERC*Pointer hardhat suites and the pointer-backed dapp and rpc scenarios
are gone, and pointerview.spec.ts now covers unregistered lookups only.
A co-located CosmWasm transfer produces a shell receipt only when its
contract has a pointer, so the rpc fixtures assert the cumulative-gas
series as an exact running sum rather than tolerating a jump.

Co-authored-by: Cursor <cursoragent@cursor.com>
#4213 removed the precompile snapshots older than v6.6, so a traced
height below the v6.6 upgrade no longer resolves to a snapshot that
creates pointers - it resolves to the live precompile, which this branch
retires. Three pacific-1 regression fixtures trace pointer creation at
such heights and now report the retirement instead of the deploy.

0x4d9601... and 0x99d895... already expected an error and only move gas.
0xd09db4... flips: it expected success returning the new pointer address
and now traces as a revert carrying the Error(string) retirement reason,
which is pinned in full the same way #4213 pinned the retired oracle
payload.

This extends the trade-off #4213 documented by one step. It does not
reach any height at or after the v6.6 upgrade, where the retained v6.6
snapshot still replays creation faithfully.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Sep 17, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Disables a core interoperability feature (pointer registration) and changes precompile/gov behavior; existing pointers should keep working but any tooling that still creates pointers will break.

Overview
This PR retires EVM pointer creation end-to-end: no new ERC↔CW or native-bank pointer can be registered via the pointer precompile, governance, or CLI, while existing on-chain pointers and query/read paths stay.

The pointer precompile at 0x…100b remains registered but every call reverts with a clear retirement error (instead of unregistering the address, which would fail silently). Gov wiring in app switches to a stateless evm.ProposalHandler that rejects the retired Add*Pointer* proposal types; duplicate gov types under giga/deps/xevm are removed.

Solidity and CI cleanup drops the pointer contract sources, their Foundry/Hardhat tests, pointer helpers in contracts/test/lib.js, and the Forge GitHub workflow (no .t.sol left). Integration matrix rows that ran EVM Interoperability (Pointer Tests) are removed. evmrpc regression fixtures are updated so historical pointer-creation traces now expect the retirement revert.

Docs/examples point local Hardhat testing at EVMCompatabilityTest.js instead of pointer suites.

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

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 17, 2026, 1:48 PM

@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 2 potential issues.

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 0e091b8. Configure here.

Comment thread integration_test/precompile_tests/precompiles/pointer.spec.ts Outdated
it('block.gasUsed equals Σ receipt gasUsed and cumulativeGasUsed is consistent', async () => {
const block = await byHash(sei, richSei.hash, true);
await assertGasAccounting(sei, block, richSei.cosmosShellGas);
await assertGasAccounting(sei, block);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gas checks drop Cosmos contribution

Medium Severity

Rich-block gas assertions now equate block.gasUsed to the EVM receipt sum and no longer pass cosmosShellGas. buildRichSeiBlock still co-locates a Cosmos CW20 transfer, and those non-EVM txs contribute to block.gasUsed, so the equality fails or flakes on dual-VM blocks.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by learned rule: test: Sei RPC block-level assertions must account for non-EVM txs in dual-VM blocks

Reviewed by Cursor Bugbot for commit 0e091b8. Configure here.

@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.89%. Comparing base (0b253c2) to head (e67a9c1).

Files with missing lines Patch % Lines
precompiles/pointer/pointer.go 78.57% 3 Missing ⚠️
x/evm/migrations/retired_pointer_boundary.go 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4230      +/-   ##
==========================================
- Coverage   66.91%   65.89%   -1.02%     
==========================================
  Files        2176     2056     -120     
  Lines      167167   154972   -12195     
==========================================
- Hits       111858   102121    -9737     
+ Misses      55168    52710    -2458     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 61.90% <91.66%> (?)
sei-db 74.50% <ø> (ø)
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/app.go 77.50% <100.00%> (+0.40%) ⬆️
evmrpc/tests/tx.go 86.86% <ø> (+8.68%) ⬆️
occ_tests/messages/test_msgs.go 93.22% <ø> (+2.49%) ⬆️
x/evm/client/cli/native_tx.go 39.28% <ø> (+3.80%) ⬆️
x/evm/client/cli/tx.go 31.77% <ø> (-0.36%) ⬇️
x/evm/handler.go 20.00% <100.00%> (+8.00%) ⬆️
x/evm/module.go 51.16% <100.00%> (+4.35%) ⬆️
x/evm/types/errors.go 100.00% <ø> (ø)
x/evm/types/gov.go 100.00% <100.00%> (+81.03%) ⬆️
x/evm/migrations/retired_pointer_boundary.go 0.00% <0.00%> (ø)
... and 1 more

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

alexander-sei and others added 2 commits September 17, 2026 12:46
IBC.sol, Oracle.sol and Pointer.sol are reference interfaces published
alongside their precompiles for contract authors to import. All three
precompiles now refuse every call, so the interfaces describe an API that
cannot be exercised: a contract written against one compiles and then
reverts at runtime.

Nothing in the tree imports them and no script, doc or CI job names them.
The interfaces of the seventeen live precompiles are untouched, as is
every Solidity source under contracts/src - CW20ERC20Pointer.sol and
WSEI.sol have no importer either, but solc builds the bytecode embedded in
x/evm/artifacts from them.

Co-authored-by: Cursor <cursoragent@cursor.com>
seidroid[bot]
seidroid Bot previously requested changes Sep 17, 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.

The retirement itself is well-executed and faithfully follows the existing precompiles/ibc/retiredibc tombstone patterns, with the gov-registry and migration-sequence reasoning correct. Two blockers remain: CI still invokes a deleted test script, and the new retirement integration test asserts an error string the precompile never produces.

Findings: 2 blocking | 2 non-blocking | 1 posted inline

Blockers

  • .github/workflows/integration-test-matrix.json still lists ./integration_test/evm_module/scripts/evm_interoperability_pointer_tests.sh at lines 54 and 75 (rows "EVM Interoperability (Pointer Tests)" and "Autobahn EVM Interoperability (Pointer Tests)"), but the PR deletes that script. Both matrix jobs will fail immediately with "No such file or directory". Since the script's entire body was the four deleted ERC*Pointer hardhat suites, both rows should be removed from the matrix along with it.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] contracts/README.md:5 still documents npx hardhat test --network seilocal test/ERC20toCW20PointerTest.js, which this PR deletes. Point the example at a suite that still exists.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] contracts/test/lib.js:525 proposeCW20toERC20Upgrade shells out to seid tx evm add-cw-erc20-pointer, a CLI command that no longer exists (removed with the CW-wrapper direction). It is exported but has no callers, so it is dead code that would fail if reused.

Comment thread integration_test/precompile_tests/precompiles/pointer.spec.ts Outdated
…emoval

Two rows in .github/workflows/integration-test-matrix.json, "EVM
Interoperability (Pointer Tests)" and its Autobahn twin, ran the
evm_interoperability_pointer_tests.sh script this branch deletes; both jobs
would have failed on a missing file. The script's whole body was the four
ERC*Pointer hardhat suites that went with the creation path, so the rows go
with it.

pointer.spec.ts asserted the VmError contained "EVM pointer creation is
deprecated", a string from an earlier draft of the precompile. It reverts
with ErrPointerPrecompileRetired, "pointer precompile is retired; EVM
pointer creation is disabled", so the assertion could never hold. It now
matches on the retirement text the way oracle.spec.ts does.

contracts/README.md pointed its worked example at a deleted suite, and
lib.js kept proposeCW20toERC20Upgrade, which drives the
add-cw-erc20-pointer command that went away with the CosmWasm wrapper
direction in #4198. It had no callers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alexander-sei

Copy link
Copy Markdown
Contributor Author

Both seidroid blockers were real and are fixed in c19b3f0.

The matrix rows are gone — integration-test-matrix.json now has 24 rows and every script it names exists on disk, checked programmatically rather than by eye. pointer.spec.ts asserts 'pointer precompile is retired', matching the oracle.spec.ts convention; the old constant was a string from an earlier draft of the precompile and nothing in the Go CI could have caught it, since the TypeScript suites only run against a live node. Also took both non-blocking suggestions: contracts/README.md points its example at EVMCompatabilityTest.js, and proposeCW20toERC20Upgrade is deleted.

On Bugbot's "Gas checks drop Cosmos contribution" — I believe that one is a false positive, and I'd rather leave it here than have someone re-add the tolerance later. A co-located CosmWasm tx only gets a shell receipt when its contract has a registered pointer: app/receipt.go builds synthetic logs solely inside the three if exists pointer branches and returns early at if len(logs) == 0. The bootstrap no longer registers a pointer for the fixture CW20, so that tx now produces no receipt at all, contributes nothing to cumulativeGasUsed, and the series is exactly the running sum of EVM receipt gas. The block.gasUsed == Σ receipts equality the comment worries about is also untouched by this PR — it held before and still holds, because blockGasUsed in evmrpc/block.go only ever accumulated receipt gas. Dropping cosmosShellGas made the assertion stricter, not looser.

alexander-sei and others added 2 commits September 17, 2026 14:12
Nothing reads these. x/evm/artifacts embeds the checked-in .abi and .bin, so the
Go build never touches a .sol, and the pointer bytecode is frozen: it can never
be redeployed, so a rebuild could only produce something that differs from what
is on chain.

Deleting the four pointer sources takes their three Foundry tests with them,
since those import them, and that orphans the four interfaces under
contracts/src/precompiles, which nothing else imported. They were the repo's
only .t.sol files, so forge-test.yml would have built and tested an empty
project and goes too. foundry.toml and contracts/lib stay: five sources under
contracts/src still resolve @OpenZeppelin through them, and forge build remains
the documented way to produce artifacts.

x/evm/artifacts/README described regenerating the artifacts with solc from
sources that no longer exist; it now records that they are frozen and why. The
Foundry and hardhat instructions in contracts/README.md lose the pointer
rebuild section, and example/contracts/README loses its pointer solc line.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alexander-sei

Copy link
Copy Markdown
Contributor Author

@seidroid review

@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 pointer retirement is thorough and internally consistent — precompile tombstone, gov proposal/route retention, inert migration boundaries, CLI removal and fixture updates all line up, and both previously reported blockers are fixed. Only non-blocking notes remain: two new tests assert less than their names claim, and the revert-reason encoder is duplicated a third time.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] integration_test/dapp_tests/nftMarketplace/nftMarketplaceTests.js: inside testNFTMarketplaceOrder, nftOwner is declared with let inside the if (seller.address !== deployer.address) block but reassigned after it (nftOwner = await nftContract.ownerOf(tokenId)), creating an implicit global. It works in CommonJS sloppy mode but would throw under strict mode/ESM. Declaring it at function scope fixes it.

false,
false,
)
require.ErrorIs(t, err, vm.ErrExecutionReverted)

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 test passes whether or not ValidateNonPayable is still called: every call to the retired precompile returns vm.ErrExecutionReverted, so removing the non-payable guard would not fail TestRetiredPrecompileRejectsValue.

ValidateNonPayable returns errors.New("sending funds to a non-payable function") (precompiles/common/precompiles.go:261) and short-circuits before the retirement error, so the two are distinguishable. Asserting on statedb.GetPrecompileError() — that it contains the non-payable message and is not pointer.ErrPointerPrecompileRetired — makes the test actually pin the guard, mirroring how TestRetiredPrecompileRejectsCalls above asserts ErrorIs(statedb.GetPrecompileError(), pointer.ErrPointerPrecompileRetired).

pointer.addCW20Pointer.staticCall(runtime.funded.adminSeiAddress),
'pointer.addCW20Pointer with a non-contract address',
);
it('the CW methods revert', async () => {

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 case does not pin the retirement. expectExecutionReverted only matches /execution reverted|revert/i, and before the retirement addCW20Pointer/addCW721Pointer/addCW1155Pointer already reverted for a valid non-contract sei address (sdk.AccAddressFromBech32 succeeded, then QuerySmartSafe failed) — so this test would pass unchanged against a build where creation still works.

expectExecutionReverted returns the error message, so asserting it contains RETIRED (already defined at line 28) makes the three CW selectors as load-bearing as the addNativePointer case above. The same applies to the dispatch semantics loop at line 84: all three entrypoints revert regardless, so nothing there distinguishes the retirement from the pre-existing staticcall/delegatecall guards.

return nil, 0, err
}
res, err := p.wasmdKeeper.QuerySmartSafe(ctx, cwAddress, []byte("{\"token_info\":{}}"))
func mustEncodeRevertReason(reason string) []byte {

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] mustEncodeRevertReason is now the third byte-identical copy in non-generated code — precompiles/oracle/oracle.go:115 and precompiles/ibc/ibc.go:69 have the same nine lines. Both files already import pcommon, so hoisting it there (e.g. pcommon.MustEncodeRevertReason) collapses all three; the copies under precompiles/*/legacy/* are frozen snapshots and should stay as they are.

Worth doing now rather than at the fourth retirement: the Error(string) selector and the argument packing are exactly the kind of detail that drifts between copies unnoticed, since each is exercised only by its own precompile's tests.

@seidroid
seidroid Bot dismissed their stale review September 17, 2026 15:47

Superseded: latest AI review found no blocking issues.

@alexander-sei
alexander-sei requested a review from masih September 17, 2026 15:51
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