fix: F-2026-18195 | [Dual Defense] isCEA Smart-Contract Inbound Lifecycle Gaps Strand Bridged Principal - #319
Open
0xNilesh wants to merge 2 commits into
Open
fix: F-2026-18195 | [Dual Defense] isCEA Smart-Contract Inbound Lifecycle Gaps Strand Bridged Principal#3190xNilesh wants to merge 2 commits into
0xNilesh wants to merge 2 commits into
Conversation
Attach inside the callback CacheContext so a nested UniversalGatewayPC burn and its OutboundTx/PendingOutbounds rows commit atomically; a failed attach discards the cache and records a FAILED PcTx.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
Hacken's F-2026-18195 bundles two defects. This PR fixes only the second one.
Fixed here — the missing outbound attach on a successful smart-contract callback.
On the
isCEAsmart-contract recipient branch, when the callback succeeded the handler recorded thePcTx and returned without calling
AttachOutboundsToExistingUniversalTx. If the recipientcontract called
UniversalGatewayPCduring its callback, the PRC20 was burned and aUniversalTxOutboundlog emitted — but noOutboundTxand noPendingOutboundsrow was evercreated, so nothing was signed or delivered.
DerivedEVMCallskipsPostTxProcessing, so the EVMhook does not compensate. Every step reported SUCCESS, so rescue and remint were both ineligible:
burned supply with nothing to deliver.
The UEA branch in the same function already attached. There was no comment or documented rationale
for the difference — this was an asymmetry, not a design choice.
Not changed — the
isCEAroute performs no automatic revert. That is accepted behaviour by design.Recovery is manual, via the rescue route, when the token mint fails. When the mint succeeds and the
payload then fails, the principal remains with the recipient the sender nominated and under their
control. The sender chooses the CEA recipient and is spending their own funds, so this is integrator
responsibility rather than a protocol defect. The choice is explicit in the code
(
// isCEA failures never create an INBOUND_REVERT outbound). NoINBOUND_REVERTwas added to theisCEA path, rescue eligibility was not widened, and the deposit-commit ordering is untouched.
The change
Both affected handlers, and only these two files:
x/uexecutor/keeper/execute_inbound_funds_and_payload.gox/uexecutor/keeper/execute_inbound_gas_and_payload.goOn smart-contract callback success, outbounds are now attached from
contractReceipt— inside theexisting
cacheCtx, beforewriteCache().Why inside the cache
The gateway burn happens in
cacheCtx. Attaching there means the burn and the resultingOutboundTx/PendingOutboundsrows land in the same cache and commit together onwriteCache(),or not at all.
CacheContext()also gives the branch its ownEventManager, whichwriteCache()flushes to the parent, so the
OutboundCreatedevent is atomic with the rows it describes. There isno window in which supply is burned but no outbound exists.
If the attach fails,
writeCache()is not called: the whole callback — including the burn and thegas-fee deduction — is rolled back, and the PcTx is recorded as
FAILEDwith the attach error underan
outbound attach failed: ...prefix.Why the
RevertErrorpattern was not copiedThe UEA branch stashes attach errors in
UniversalTx.RevertError, a field with 9 writes and 0 readschain-wide — it silently loses the failure, and leaves the burn committed with no outbound. Copying
that here would have reproduced the exact stranding this PR is meant to remove. The failure is put on
the PcTx instead, where operators and the audit trail already look, and the state is rolled back so
there is nothing stranded to reconcile. The UEA branch itself is unchanged.
Tests
New:
test/integration/uexecutor/inbound_cea_contract_outbound_test.go.A mock recipient contract re-enters
UniversalGatewayPCduring its callback (raw runtime bytecode:SSTOREa witness into slot 0, thenCALLthe gateway with a fixedwithdrawcalldata blob). Theslot-0 witness is what proves commit vs. rollback of the callback body.
OutboundTx(destination chain, recipient, amount, PRC20,PENDING) and a populatedPendingOutboundsentry pointing at the UTX.BuildOutboundsFromReceiptfail. The callback is fully rolled back (slot 0 stays0), noOutboundTx, noPendingOutboundsrow, no gas fee collected, and the PcTx isFAILEDcarryingoutbound attach failed: ... outbound is disabled for chain .... The deposit, which happens beforethe cache scope, stays committed.
SUCCESSPcTx with no spuriousoutbound or
PendingOutboundsrows.GAS_AND_PAYLOADcases use a zero inbound amount so the handler skipsgasAndPayloadDepositAutoSwap,which needs a live Uniswap quoter/router the integration harness does not deploy.
isSmartContractisset from the recipient's code hash regardless of amount, so the branch under test is still exercised.
Each new case was verified as a genuine regression detector: with the fix reverted, the four
attach-related cases fail (missing
OutboundTx; PcTxSUCCESSinstead ofFAILED) and the tworegression cases still pass.
Full
./test/integration/...(uexecutor, upgrades, uregistry, utss, uvalidator) is green.Reply to Hacken