Skip to content

Retire all versions of IBC precompiles and remove legacy implementations - #3884

Draft
masih wants to merge 4 commits into
mainfrom
masih/ibc-retire-gracefully
Draft

Retire all versions of IBC precompiles and remove legacy implementations#3884
masih wants to merge 4 commits into
mainfrom
masih/ibc-retire-gracefully

Conversation

@masih

@masih masih commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Retire the IBC precompile at every version while preserving each historical ABI and the registered address. All valid calls now revert with a clear retirement reason.

Remove obsolete legacy IBC implementations and unused keeper dependencies. Teach the version generator to skip retired modules, preventing IBC from being archived or reactivated during future upgrades.

Add coverage confirming every registered IBC version reverts and remains non-payable.

Retire the IBC precompile at every version while preserving each historical
ABI and the registered address. All valid calls now revert with a clear
retirement reason.

Remove obsolete legacy IBC implementations and unused keeper dependencies.
Teach the version generator to skip retired modules, preventing IBC from being
archived or reactivated during future upgrades.

Add coverage confirming every registered IBC version reverts and remains
non-payable.
@github-actions

github-actions Bot commented Aug 8, 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 10, 2026, 2:53 PM

@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.74%. Comparing base (a115971) to head (a30958f).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3884      +/-   ##
==========================================
- Coverage   61.61%   60.74%   -0.87%     
==========================================
  Files        2348     2253      -95     
  Lines      200852   190078   -10774     
==========================================
- Hits       123755   115472    -8283     
+ Misses      66044    64406    -1638     
+ Partials    11053    10200     -853     
Flag Coverage Δ
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/app.go 71.52% <ø> (-0.04%) ⬇️
app/precompiles.go 100.00% <ø> (ø)
evmrpc/simulate.go 76.41% <ø> (-0.05%) ⬇️
evmrpc/trace_baker.go 77.93% <ø> (+0.25%) ⬆️
evmrpc/trace_profile.go 65.93% <ø> (ø)
evmrpc/tracers.go 69.38% <ø> (-0.31%) ⬇️
giga/deps/xevm/keeper/keeper.go 61.47% <ø> (-0.34%) ⬇️
precompiles/ibc/ibc.go 100.00% <ø> (+49.56%) ⬆️
precompiles/utils/expected_keepers.go 100.00% <ø> (ø)
scripts/bump_version/main.go 0.00% <ø> (ø)
... and 2 more

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

@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Disables a cross-chain transfer path for EVM contracts; behavior is intentional but breaks any dapp still calling the IBC precompile.

Overview
Permanently disables EVM IBC transfers by replacing the live IBC precompile and every historical version with a shared retired implementation that keeps address 0x…1009 and each ABI registered but makes all valid calls revert with ibc precompile is retired; ibc transfers are disabled (via new NewRetiredPrecompile in precompiles/common).

Removes IBC wiring from the node: TransferKeeper is no longer passed into EvmKeeper / GigaEvmKeeper or PrecompileKeepers, and IBC client/connection/channel/transfer keeper interfaces are dropped from legacy precompile expected_keepers. All precompiles/ibc/legacy/v* transfer implementations are deleted.

Tests now assert every entry from ibc.GetVersioned reverts with the retirement reason and that the precompile still rejects payable calls.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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 add06b2. Configure here.

Comment thread precompiles/ibc/setup.go

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: add06b2823

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread precompiles/ibc/setup.go Outdated
versioned := make(utils.VersionedPrecompiles, len(legacyABIByVersion)+1)
versioned[latestUpgrade] = newRetiredPrecompile(pcommon.MustGetABI(currentABI, "abi.json"), keepers)
for version, filename := range legacyABIByVersion {
versioned[version] = newRetiredPrecompile(pcommon.MustGetABI(legacyABIs, filename), keepers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve historical IBC executors for tracing

When debug_traceTransaction replays a pre-v6.6 transaction that successfully called the IBC precompile, CustomPrecompiles selects one of these height-specific entries, but every entry now uses the retired executor and reverts rather than reproducing the original call and state transitions. Keeping only each historical ABI is insufficient for replay correctness; retain the legacy executors for historical versions and retire only the active version.

Useful? React with 👍 / 👎.

seidroid[bot]
seidroid Bot previously requested changes Aug 8, 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.

Retiring the IBC precompile while preserving every historical ABI and the registered address is a clean approach, and the dead-keeper cleanup is thorough with no dangling references. One blocking issue: opting the module out of scripts/bump_version leaves legacyABIByVersion hand-maintained, and it is already missing v6.6, which will resolve to a nil precompile for the IBC address as soon as the next tag is cut.

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

Blockers

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

Non-blocking

  • The Cursor second-opinion file (cursor-review.md) is empty — that pass produced no output, so this review merges only Claude's and Codex's findings. Codex also reported it could not run tests (Go 1.25.6 toolchain download blocked); I could not build either, so all findings here are from static reading.
  • precompiles/ibc/versions is now orphaned: discoverModules() skips retired modules, so archiveModule will never append future tags to it, yet x/evm/keeper/keeper_test.go:141 (TestGetCustomPrecompiles) still reads it. Either delete the file or make it the source of truth for legacyABIByVersion — the latter also closes the v6.6 gap permanently.
  • Test coverage exercises only the transferWithDefaultTimeout selector. The other ABI methods, an unknown selector, and sub-4-byte calldata all take different paths through RunAndCalculateGas (revert with no reason data rather than the retirement reason); worth one case each.
  • Every retired call now reaches HandlePrecompileError plus the unconditional fmt.Printf("precompile %s encountered error: ...") at precompiles/common/precompiles.go:161 and increments error metrics. A precompile that reverts by design turns that pre-existing log line into a guaranteed per-call stdout write and error-metric bump that any caller can drive cheaply. Consider whether retirement should bypass the error-reporting path.
  • Confirmation request, not a correctness claim: retirement is applied to every historical version, so tracing/replaying blocks that originally executed a successful IBC transfer will now revert instead of reproducing the committed result. Per REVIEW_GUIDELINES §2 I am not treating this as a bug, but the retroactive scope (as opposed to gating retirement at the next upgrade) is worth an explicit sign-off in the PR description given the app-hash-breaking label.
  • 6 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread precompiles/ibc/setup.go Outdated

moduleName := entry.Name()
moduleDir := filepath.Join(precompilesDir, moduleName)
if fileExists(filepath.Join(moduleDir, retiredMarker)) {

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 retirement guard now exists in two places — here and in discoverModules() (line 534). regenerateAllSetup re-walks precompilesDir itself rather than iterating discoverModules(), so the next person adding a module-wide filter has to remember both.

AGENTS.md "Structural corrections" calls for exactly this: guard at the single choke point every path passes through. Having regenerateAllSetup iterate discoverModules() (and keep only its versions/{name}.go checks) makes the exclusion an invariant instead of a convention.

Comment thread precompiles/ibc/ibc_test.go Outdated
evm := &vm.EVM{StateDB: state.NewDBImpl(ctx, &testApp.EvmKeeper, true)}

versioned := ibc.GetVersioned("v6.6", testApp.GetPrecompileKeepers())
require.Len(t, versioned, 15)

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] require.Len(t, versioned, 15) is the assertion that should have caught the missing v6.6 mapping, and it can't: the count is a literal, and "v6.6" is passed in as latestUpgrade on line 24 so it's present regardless.

Read precompiles/ibc/versions and assert that the key set of GetVersioned(<last line>, …) equals the set of lines in that file. That fails loudly when a tag is added without a corresponding ABI entry — which is now a purely manual step for this module.

Comment thread precompiles/ibc/ibc.go
const (
IBCAddress = "0x0000000000000000000000000000000000001009"
)
const RetiredReason = "ibc precompile is retired; ibc transfers are disabled"

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] "ibc transfers are disabled" overstates the change. The IBC transfer module is still mounted and routed (app/app.go:624, :636, :867), so native Cosmos IBC transfers keep working — only the EVM precompile is retired. This string is surfaced to contract callers as the revert reason, so it's worth being precise, e.g. "ibc precompile is retired; use native ibc transfers".

Comment thread precompiles/common/retired.go Outdated
if err := ValidateNonPayable(value); err != nil {
return nil, 0, err
}
return e.revertData, GetRemainingGas(ctx, e.evmKeeper), e.err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] e.revertData is a single slice built once at construction and handed to every caller. Nothing in the current EVM path mutates returned ret, so this isn't a live bug, but the aliasing is invisible from the call site and one in-place write would corrupt the revert reason for every subsequent call on that node. return common.CopyBytes(e.revertData), … removes the question.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@masih I could imagine some possible issue with OCC here where some weird caching somewhere could lead to a consensus divergence. worth a look maybe

Comment thread precompiles/common/retired.go Outdated

func (e *retiredExecutor) Execute(ctx sdk.Context, _ *abi.Method, _ common.Address, _ common.Address, _ []interface{}, value *big.Int, _ bool, _ *vm.EVM, _ uint64, _ *tracing.Hooks) ([]byte, uint64, error) {
if err := ValidateNonPayable(value); err != nil {
return nil, 0, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] The non-payable rejection returns nil revert data, so a value-bearing call reverts with no reason at all — TestRetiredIBCPrecompileRemainsNonPayable pins that with require.Empty(t, ret). Returning all-gas-consumed matches the convention of the other executors, but the empty reason is at odds with the stated goal that calls revert with a clear retirement reason; the caller sending value gets less information than the caller who doesn't. Consider returning e.revertData here too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something worth to improve, some precompile functions are payable and are also used as such

Comment thread tools/utils/helper.go
minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,
evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey, oracletypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,

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] Dropping ibchost.StoreKey / ibctransfertypes.StoreKey here (and "ibc" / "transfer" from Modules below) goes further than retiring the precompile: both modules are still mounted in app/app.go, so these lists no longer describe the app's actual stores. tools/utils has no importer in-tree today, so the impact is nil, but if it's ever used for a state dump or size report it will silently omit live IBC state. Retiring the precompile doesn't require this hunk.

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

Retiring the IBC precompile while keeping 0x1009 registered and preserving every historical ABI is the right shape, and the dangling keeper wiring is removed cleanly with no leftover references. No blockers; the notes below concern the hand-written GetVersioned diverging from the generator's manifest contract, unasserted gas/revert semantics, a duplicated retired-marker guard, and an out-of-scope change to tools/utils.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Neither new test asserts the returned remainingGas, yet retirement changes it: previously every IBC failure returned 0 (all supplied gas burned), now the normal revert path returns the real remaining gas. Gas is consensus-relevant on an app-hash-breaking change — pin the expected gas outcome for both the reverting and the value-bearing path.
  • TestEveryIBCVersionIsRetired only exercises transferWithDefaultTimeout. The other registered method (transfer) is never called for any version, so a selector-decoding regression on transfer would go unnoticed. Iterating over contractABI.Methods instead of hardcoding one name would cover both and stay correct as ABIs differ across versions (v5.5.2 lacks memo).
  • precompiles/ibc/IBC.sol is unchanged and still presents transfer/transferWithDefaultTimeout as functional. Keeping the file is correct (the ABI must be preserved), but a retirement note in the interface doc would stop integrators writing against a permanently-reverting contract.
  • RunAndCalculateGas does fmt.Printf("precompile %s encountered error: ...") on every error. Retirement makes that the guaranteed outcome for a publicly callable address, so every call now writes a line to validator stdout. Pre-existing code path, but the cost/benefit changes when it is the only possible outcome — worth considering demoting it for retired precompiles.
  • Cursor's review file (cursor-review.md) is empty — that pass produced no output. Codex reported no material issues, matching my read on correctness.
  • I could not compile or run the test suite in this environment (Go toolchain fetch is network-blocked, same limitation Codex hit). Compile-correctness of the NewKeeper signature changes and the removed TransferK()/ClientK()/ConnectionK()/ChannelK() interface methods was verified by grepping for dangling references (none found in precompiles/, x/evm/, giga/, tools/, app/), not by a build.
  • 5 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread precompiles/ibc/setup.go
"v6.5": check(ibcv65.NewPrecompile(keepers)),
historicalVersions := getHistoricalVersions()
versioned := make(utils.VersionedPrecompiles, len(historicalVersions)+1)
for _, version := range historicalVersions {

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 loop treats every line of versions as a legacy version, but the generator's contract is that the last manifest entry is the active version, not an archived one — regenerateAllSetup uses legacyCount := len(versions) - 1 and keys that final entry via latestUpgrade (scripts/bump_version/main.go:412). The generated setup.go this replaces stopped at v6.5 for exactly that reason, even though versions ends with v6.6.

Consequences today are benign — app/tags ends at v6.6, so line 26 overwrites the loop's v6.6 entry and both resolve to byte-identical ABIs (legacy/v66/abi.json == abi.json). But two things follow:

  • In production the map has len(versions) entries, not len(versions)+1, so require.Len(t, versioned, len(historicalVersions)+1) in ibc_test.go:32 only passes because the test injects a synthetic "future-upgrade". It asserts a shape the running node never produces, and will silently start being the real shape once the next tag is cut.
  • historicalVersions is a misnomer: it includes the current version.

Suggest iterating historicalVersions[:len(historicalVersions)-1] (mirroring the generator) and naming it accordingly, or documenting deliberately why the whole manifest is archived here.

Comment thread precompiles/common/retired.go Outdated
if err := ValidateNonPayable(value); err != nil {
return nil, 0, err
}
return e.revertData, GetRemainingGas(ctx, e.evmKeeper), e.err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] The two exit paths of this executor have inconsistent semantics. A value-bearing call takes the branch above and returns nil, 0, err — all supplied gas burned, and no revert data, so a Solidity caller sees a bare revert() with no reason. Every other call returns the encoded reason plus the real remaining gas, so it reverts cheaply and explains itself. TestRetiredIBCPrecompileRemainsNonPayable pins the empty-ret half of that asymmetry without noting it is intentional.

The PR description says "All valid calls now revert with a clear retirement reason"; a call carrying value is exactly the case where a caller most needs the reason (their funds are the thing being rejected). Consider returning e.revertData for the non-payable rejection too, or add a comment stating why the payable path deliberately keeps the older burn-all-gas behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same as above with the payable

func validCallData(t *testing.T, contractABI abi.ABI) []byte {
t.Helper()

method := contractABI.Methods["transferWithDefaultTimeout"]

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] Single-value map index: if a version's ABI ever lacked transferWithDefaultTimeout, this yields the zero abi.Method — nil ID, empty Inputs — so input becomes empty, ExtractMethodID fails with "input too short", and the test fails at abi.UnpackRevert with a message that points nowhere near the actual cause. Use the two-value form with require.True(t, ok, ...) so the failure names the missing method. (All 15 archived ABIs do currently define it, so this is about the next one, not today.)


moduleName := entry.Name()
moduleDir := filepath.Join(precompilesDir, moduleName)
if fileExists(filepath.Join(moduleDir, retiredMarker)) {

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 retired-marker check now lives in two places — here and in discoverModules at line 534 — because regenerateAllSetup re-walks precompilesDir itself instead of going through discoverModules(). That is the pattern AGENTS.md calls out under "Guard at the choke point, never at each caller": a third module walker added later has to remember the marker, where routing both through one discovery function makes it an invariant. regenerateAllSetup also duplicates the excludeDirs filter for the same reason.

Suggest having regenerateAllSetup iterate discoverModules() so the marker (and excludeDirs) are honoured in exactly one place.

Comment thread tools/utils/helper.go
minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,
evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey, oracletypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,

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] Dropping ibchost.StoreKey / ibctransfertypes.StoreKey here (and "ibc" / "transfer" from Modules below) makes this helper disagree with the running app: app/app.go:276-277 still mounts both stores, and the IBC and transfer modules are still registered in the module manager (app/app.go:897, 967-970). Those stores still hold state, so a state-size or dump tool built on ModuleKeys/Modules will now silently omit it and under-report rather than error.

Nothing in-tree imports tools/utils today, so this is not an active break — but it is also unrelated to retiring the precompile, and the precompile retirement does not remove the IBC module. Worth either reverting this hunk from the PR or stating why the tool should stop seeing state the chain still keeps.

@seidroid
seidroid Bot dismissed their stale review August 8, 2026 21:18

Superseded: latest AI review found no blocking issues.

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

Retiring the IBC precompile at every version while preserving the address and each historical ABI is a clean, well-documented approach, and the removal of ~7.4k lines of legacy code and unused keepers checks out (no dangling references, all embedded legacy ABIs present). No blockers; the notes below are a mix of a manifest off-by-one against the bump_version convention, gas/log-amplification asymmetries in the new retired executor, an out-of-scope tools/utils change, and test/doc gaps.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Historical trace fidelity is permanently lost: the versioned precompile map is consumed only under ctx.IsTracing() (x/evm/keeper/keeper.go:174-182), so debug_trace* / replay of any historical block containing a successful IBC precompile call will now report a revert, with different gas accounting. This appears intentional ("Retire the IBC precompile at every version") and per REVIEW_GUIDELINES.md §2 I'm not treating it as a correctness bug — but it is irreversible (the legacy implementations are deleted, not just unwired) and should be stated explicitly in the PR description / upgrade notes for anyone relying on historical tracing.
  • CHANGELOG.md ## Unreleased is not updated. This is both client-breaking (calls to 0x…1009 now revert) and state-machine-breaking, and the section is actively maintained (e.g. the #3818 entry), so it warrants an entry under the appropriate stanza.
  • precompiles/ibc/IBC.sol is left unchanged and still advertises working transfer / transferWithDefaultTimeout. It is the developer-facing artifact for this address; a deprecation comment there (and in the retained abi.json docs if any) would prevent integrators from writing against a permanently reverting interface.
  • The new retired marker convention is documented only in scripts/bump_version/README.md. A line in the top-level AGENTS.md (or a precompiles/AGENTS.md) would make it discoverable to contributors and agents working under precompiles/, since it changes how the version generator behaves.
  • Second-opinion passes: Codex reported "No material issues found in the PR diff." Cursor's cursor-review.md is empty — that pass produced no output, so it provides no signal either way.
  • Verification caveat: go build ./... and go test ./precompiles/ibc/... were blocked in this sandbox, so compile/test confirmation is by inspection only. I did grep-verify that no references remain to the removed TransferKeeper/ClientKeeper/ConnectionKeeper/ChannelKeeper interfaces or the deleted legacy IBC packages (including in the other precompiles/common/legacy/* dirs), that ibc.IBCAddress is still consumed by precompiles/setup.go and giga/executor/precompiles/failfast.go, and that all 15 precompiles/ibc/legacy/*/abi.json files required by the new go:embed pattern exist. Please rely on CI for the actual build/test result.
  • 8 suggestion(s)/nit(s) flagged inline on specific lines.


func (e *retiredExecutor) Execute(ctx sdk.Context, _ *abi.Method, _ common.Address, _ common.Address, _ []interface{}, value *big.Int, _ bool, _ *vm.EVM, _ uint64, _ *tracing.Hooks) ([]byte, uint64, error) {
if err := ValidateNonPayable(value); err != nil {
return common.CopyBytes(e.revertData), 0, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Asymmetric gas semantics between two paths that produce the identical outcome. A value-bearing call returns remainingGas = 0 (all supplied gas consumed) while the plain retirement revert on line 37 returns GetRemainingGas(...) (gas refunded). Both return the same Error("ibc precompile is retired; ...") revert data and both surface as vm.ErrExecutionReverted, so a caller sending 1 wei is charged the full gas limit and a caller sending 0 is not, for the same revert.

A precompile that unconditionally reverts has no payability semantics left to enforce, so the simplest correction is to drop the ValidateNonPayable branch entirely and always return common.CopyBytes(e.revertData), GetRemainingGas(ctx, e.evmKeeper), e.err. That also makes the retirement reason the single recorded error for SetPrecompileError/metrics, rather than sometimes "sending funds to a non-payable function". If you prefer to keep the branch, at least align its gas return with line 37.

Worth noting: several historical ABIs declare both methods "stateMutability": "payable" (e.g. legacy/v552/abi.json), so TestRetiredIBCPrecompileRemainsNonPayable is not quite describing what it pins — the retired executor imposes non-payability uniformly, including on versions where the method was payable.

if err := ValidateNonPayable(value); err != nil {
return common.CopyBytes(e.revertData), 0, err
}
return common.CopyBytes(e.revertData), GetRemainingGas(ctx, e.evmKeeper), e.err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Because this executor errors on every call, each call to 0x…1009 now unconditionally triggers three logging/metric side effects that previously only fired on genuine failures:

  • the unconditional fmt.Printf("precompile %s encountered error: %v\n", ...) in DynamicGasPrecompile.RunAndCalculateGas (precompiles/common/precompiles.go:161) — raw stdout, no rate limiting;
  • metrics.IncrementErrorMetrics(operation, err) via HandlePrecompileError, which will now light up error dashboards/alerts for what is expected behaviour;
  • if a contract try/catches the revert and the transaction succeeds, logger.Error("Transaction succeeded in execution but has precompile error ", ...) in x/evm/keeper/receipt.go:283.

A reverting precompile call is cheap, so this is unbounded stdout/error-log and error-metric amplification on every node, triggerable by anyone. None of these lines are introduced here, but this change is what makes them fire on the happy path. Removing (or gating) that fmt.Printf and considering whether a retirement revert should count toward error metrics would be worth doing in this PR.

Comment thread precompiles/ibc/setup.go
filename := "legacy/" + strings.ReplaceAll(version, ".", "") + "/abi.json"
versioned[version] = newRetiredPrecompile(pcommon.MustGetABI(retiredAssets, filename), keepers)
}
versioned[latestUpgrade] = newRetiredPrecompile(pcommon.MustGetABI(currentABI, "abi.json"), keepers)

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] Off-by-one against the versions manifest convention this file is now hand-maintaining. scripts/bump_version/main.go:411 computes legacyCount := len(versions) - 1 — the last line of versions corresponds to latestUpgrade and is deliberately not emitted as a legacy entry (compare precompiles/bank/versions, which ends in v6.6, against the generated precompiles/bank/setup.go, whose legacy list stops at v6.5).

precompiles/ibc/versions also ends in v6.6, and app.LatestUpgrade is currently v6.6 (last line of app/tags). So the loop on lines 22-25 sets versioned["v6.6"] from legacy/v66/abi.json, and this line silently overwrites it — the returned map has 15 entries, not the len(historicalVersions)+1 the make on line 21 assumes.

This is benign today (abi.json and legacy/v66/abi.json are byte-identical and every version reverts anyway) and self-resolves once a later tag is cut, but the file's own doc comment calls the manifest "the source of truth" while diverging from how every generated setup.go reads it. Either iterate historicalVersions[:len(historicalVersions)-1] to match the generator, or drop the trailing v6.6 from the frozen manifest — and either way the duplicate-assignment shadowing is worth removing before the next maintainer reads this as the reference implementation for retiring a module.


const futureUpgrade = "future-upgrade"
versioned := ibc.GetVersioned(futureUpgrade, testApp.GetPrecompileKeepers())
require.Len(t, versioned, len(historicalVersions)+1)

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 assertion only holds because the test injects the synthetic futureUpgrade = "future-upgrade" name. The production call site is precompiles/setup.go:64, ibc.GetVersioned(latestUpgrade, keepers) with latestUpgrade == app.LatestUpgrade == "v6.6", which is also the last line of versions — so in production the map has len(historicalVersions) entries and this require.Len would fail (see the related note on setup.go:26).

As written the test pins an invariant that is false for the only real caller, and specifically hides the duplicate-key collision. Adding a case that calls ibc.GetVersioned(app.LatestUpgrade, ...) would both cover the production path and surface the off-by-one.

func validCallData(t *testing.T, contractABI abi.ABI) []byte {
t.Helper()

method := contractABI.Methods["transferWithDefaultTimeout"]

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] Two test-coverage gaps here:

  1. Only transferWithDefaultTimeout is exercised, but every ABI version also exposes transfer (with differing signatures across versions). The PR description claims "All valid calls now revert"; iterating contractABI.Methods and asserting the retirement revert for each would actually prove that, and would automatically cover any method a historical ABI has that the current one doesn't.
  2. contractABI.Methods["transferWithDefaultTimeout"] is not existence-checked. On a miss this yields a zero abi.Method, so method.ID is nil and validCallData returns empty calldata — the failure then surfaces from abi.UnpackRevert rather than from the missing method, which is a confusing signal. A require.Contains(t, contractABI.Methods, ...) (or require.True(t, ok) on the two-value lookup) makes it fail where the problem is.

Minor, on line 112: append(method.ID, encoded...) appends to a slice the ABI's Method still holds (geth builds it as crypto.Keccak256(sig)[:4], so cap is 32). The transferWithDefaultTimeout payload always exceeds 28 bytes so it reallocates in practice, but append(append([]byte{}, method.ID...), encoded...) removes the aliasing question entirely.


moduleName := entry.Name()
moduleDir := filepath.Join(precompilesDir, moduleName)
if fileExists(filepath.Join(moduleDir, retiredMarker)) {

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 retired-module guard is now duplicated: here, and again inside discoverModules at line 534. AGENTS.md ("Structural corrections") calls this out directly — "Guard at the choke point, never at each caller. A guard repeated at every call site is a convention the next caller can forget, where a guard at the single function every path passes through is an invariant they cannot."

There is a choke point available: regenerateAllSetup re-implements discoverModules's filter (entry.IsDir() + excludeDirs) by walking os.ReadDir(precompilesDir) itself. Iterating discoverModules() instead gives you the identical module set plus the retired check for free, and means a third loop over precompiles/ added later cannot forget the marker. fileExists then has a single caller and the guard becomes an invariant rather than a convention.

Comment thread tools/utils/helper.go
minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,
evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey, oracletypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,

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 goes beyond retiring the precompile and looks inconsistent with the rest of the tree. ibchost.StoreKey and ibctransfertypes.StoreKey are dropped from ModuleKeys (and "ibc" / "transfer" from Modules below), but the IBC and transfer modules are still fully wired and their stores still mounted: app/app.go:276-277 mounts both store keys, app/app.go:620-637 constructs IBCKeeper/TransferKeeper and registers the transfer IBC module, app/app.go:967-970 keeps both in the module ordering, and app/ante.go:99 still installs ibcante.NewAnteDecorator. Native MsgTransfer IBC transfers remain functional; only the EVM precompile path is retired.

So the s/k:ibc/ and s/k:transfer/ subtrees still exist on chain, and any tooling driving off these lists (dump / state-size / prefix iteration, via BuildRawPrefix/BuildTreePrefix) will now silently skip them rather than report them as empty. I found no in-repo consumer of ModuleKeys/Modules, so nothing breaks at compile time, which is exactly why a silent coverage hole in external tooling is the risk. Unless the IBC Cosmos modules are also being removed (they aren't in this PR), I'd revert this file and keep it in sync with app/app.go.

Comment thread precompiles/ibc/ibc.go
const (
IBCAddress = "0x0000000000000000000000000000000000001009"
)
const RetiredReason = "ibc precompile is retired; ibc transfers are disabled"

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] "ibc transfers are disabled" overstates the change and will be inaccurate for as long as this string exists. Native Cosmos IBC transfers remain fully enabled — app/app.go:624-637 still builds TransferKeeper and registers the transfer module/IBC route, and app/ante.go:99 still installs the IBC ante decorator. What is retired is the EVM precompile path only.

This reason is ABI-encoded into on-chain revert data at a fixed address and is what dapp developers will see in their error strings, so it's worth getting precise — e.g. "ibc precompile is retired; use native ibc transfers". Same wording appears in the NewPrecompile doc comment on line 20 ("IBC transfers are permanently disabled").

@masih
masih marked this pull request as draft August 10, 2026 14:48
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.

4 participants