Skip to content

[evm/runtime]: make IntentGatewayV2 upgradeable via cross-chain governance#988

Merged
seunlanlege merged 12 commits into
mainfrom
roy/intent-gateway-upgradeable
Jun 25, 2026
Merged

[evm/runtime]: make IntentGatewayV2 upgradeable via cross-chain governance#988
seunlanlege merged 12 commits into
mainfrom
roy/intent-gateway-upgradeable

Conversation

@royvardhan

@royvardhan royvardhan commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Makes IntentGatewayV2 upgradeable by the Hyperbridge coprocessor — the same authority that already governs UpdateParams/SweepDust — through a new RequestKind.UpgradeContract handled in onAccept. The gateway runs behind an ERC1967Proxy, initialized atomically at deployment.

EVM

  • Wrap the gateway in ERC1967Proxy. The implementation stays a plain contract (no UUPS); upgrades go through ERC1967Utils.upgradeToAndCall in onAccept, gated by source == hyperbridge. The branch lives in the inherited base so it can't be dropped by a future upgrade.
  • Replace the constructor init with an initializer-guarded initialize; _disableInitializers() locks the raw implementation. The init gate is an immutable _owner (replaces storage _admin, zero slots). _filled stays at storage slot 2.
  • Initialize atomically through the proxy's initData. _instance() resolves to address(this), so cross-chain peers share the gateway's address rather than a stored registry, and the self-referential deployments array is dropped. NewDeployment remains as a governance override.
  • Append a _paused storage flag (slot 13, after all proof-sensitive slots); the pause logic is deferred.

Coprocessor

  • RequestKind::UpgradeContract { new_impl, init_data } and a GovernanceOrigin-gated upgrade_gateway extrinsic mirroring update_params. Wire byte 5 matches the EVM enum; the payload is encoded to match the Solidity abi.decode(body[1:], (address, bytes)).

Determinism

Atomic init makes the proxy address depend on the encoded Params. Those are byte-identical across chains (host, dispatcher, solverSelection uniform), so the proxy lands at the same address everywhere — which is exactly what lets _instance() resolve peers to address(this). This relies on ADMIN and Params staying uniform across chains, the discipline the deployment already follows.

Closes #981.

@royvardhan royvardhan force-pushed the roy/intent-gateway-upgradeable branch from 050a11c to 9279bbb Compare June 24, 2026 12:16
Comment thread evm/script/DeployIntentGateway.s.sol Outdated
Comment thread evm/src/apps/IntentGatewayV2.sol Outdated
Comment thread evm/script/DeployIntentGateway.s.sol Outdated
Comment thread evm/script/DeployIntentGateway.s.sol Outdated
@royvardhan royvardhan requested a review from seunlanlege June 24, 2026 14:02
Comment thread evm/script/DeployIntentGateway.s.sol
Comment thread evm/src/apps/intentsv2/IntentsBase.sol Outdated
Comment thread modules/pallets/intents-coprocessor/src/types.rs
Comment thread modules/pallets/intents-coprocessor/src/types.rs Outdated
@Wizdave97

Copy link
Copy Markdown
Member

@royvardhan

🔴 2. Atomic CREATE2 proxy deployment will revert — initialize owner gate is incompatible with the deploy flow
initialize requires msg.sender == _owner (IntentGatewayV2.sol), where _owner is the immutable admin. The production script deploys atomically:

ERC1967Proxy proxy = new ERC1967Proxy{salt: salt}(address(implementation), initData); // initData = initialize(...)
(DeployIntentGateway.s.sol:60)

Under forge script --broadcast, salted creation is routed through the deterministic CREATE2 factory (0x4e59…4956C). During the proxy constructor, ERC1967Utils.upgradeToAndCall delegatecalls initialize, and delegatecall preserves msg.sender = the CREATE2 factory — not admin. So msg.sender != _owner ⇒ revert, and the whole deployment fails (unless ADMIN is literally set to the factory address, which would make the gate meaningless).

This is masked because the tests don't exercise the atomic path — _deployGatewayProxy uses empty init data and calls initialize separately from address(this) (= the owner), with a comment claiming it "mirrors production" (IntentGatewayV2Test.sol:99-106). Production is atomic; the comment is wrong and the real path is untested.

Fix (recommended): drop the msg.sender == _owner check entirely. With atomic init, the initData is bound into the CREATE2 initcode hash, so the canonical address can only be produced by deploying the exact correct params — the owner gate adds nothing and breaks the flow. (initializer still guarantees single-init.) Then add a test that deploys through new ERC1967Proxy{salt}(impl, initData) so the atomic path is actually covered. If you instead keep separate (non-atomic) init, the owner gate is fine and front-running is harmless (front-runner isn't the owner), but the script must be changed to empty initData + a separate initialize tx.

@seunlanlege

Copy link
Copy Markdown
Member

Yeah we can just drop the owner gating

Comment thread evm/src/apps/IntentGatewayV2.sol Outdated
@seunlanlege seunlanlege merged commit e5d1de2 into main Jun 25, 2026
21 of 22 checks passed
@seunlanlege seunlanlege deleted the roy/intent-gateway-upgradeable branch June 25, 2026 12:04
Wizdave97 added a commit that referenced this pull request Jun 25, 2026
…indowExhausted in on_finalize

- indexer: ordersStorageSlot now targets _orders at slot 9. PR #988 removed
  the _admin storage slot from IntentGatewayV2, shifting _orders down from 10
  to 9. Injecting at the stale slot left the escrow override a no-op.
- pallet: move the PhantomBidWindowExhausted emission from on_initialize to
  on_finalize so a bid placed in the window-closing block is already in storage
  when the indexer aggregates the snapshot. The on_finalize storage reads are
  reserved in the on_initialize weight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[evm/runtime]: make IntentGatewayV2 upgradeable via cross-chain governance

3 participants