diff --git a/docs/ai/changelog/2026-10-10-gateway-release-4-and-official-aa-interfaces.md b/docs/ai/changelog/2026-10-10-gateway-release-4-and-official-aa-interfaces.md new file mode 100644 index 000000000..fa51b8a15 --- /dev/null +++ b/docs/ai/changelog/2026-10-10-gateway-release-4-and-official-aa-interfaces.md @@ -0,0 +1,62 @@ +# Gateway release 4 and the official ERC-4337 and Permit2 interfaces + +IntentGatewayV2 now lands proxies at `VERSION` 4. Release 4 is the implementation that binds solver +selection to the EntryPoint v0.9 UserOperation. Storage is the same as at 3. + +`migrate()` takes a proxy from 3 to 4 and writes nothing else. It takes no arguments and runs only +on a proxy exactly one version behind. The v2 path, which moved the relayer slot and set the owner, +is gone, because every live proxy is already at 3. `IIntentGatewayV2` in `@hyperbridge/core` no +longer declares `migrate`. + +Governance upgrades a gateway with `execute_on_gateway` carrying +`upgradeToAndCall(newImpl, migrate())`. `intentGatewayUpgradeInitialization` in +`evm/script/IntentGatewayScript.sol` builds that init data for a proxy at 3, and empty init data for +one already at 4. Upgrades no longer read `GATEWAY_OWNER`. + +The gateway also exposes `entrypoint()`, the EntryPoint v0.9 address whose +`getCurrentUserOpHash()` gates a selected fill. + +`fillOrder` checks each leg's shape just before filling that leg, not all legs up front. A bad later +leg still reverts the whole fill. + +The SDK's `SUPPORTED_INTENTS_VERSION` is 4. An SDK on 3 refuses a gateway once it is upgraded, and +this SDK refuses one that is not, so the SDK release and the gateway upgrades ship together. On testnet chains the SDK also accepts +release 3, because the testnet gateways already run the release-4 solver selection but still report +3. The SDK releases as `@hyperbridge/sdk` 3.0.0, a major version because it refuses every mainnet +release-3 gateway. + +The SDK's mainnet chain config points `SolverAccount` at +`0xaAd062555800a97Af062795189e32a3CBd045612` on Ethereum, BSC, Arbitrum, Base, Polygon and +Polkadot Hub. On the first five, `EntryPoint` moves to v0.9 +(`0x433709009B8330FDa32311DF1C2AFA402eD8D009`), the EntryPoint the new account validates against. +Polkadot Hub has no EntryPoint deployed. Optimism, Gnosis and Soneium keep their previous values, +since their gateways are not upgraded. Simplex reads both addresses from this config. + +The indexer lists the new account first and `0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC` second on +its five mainnet chains, so solvers keep counting while they re-delegate. +`0xd5535d4DeB17F050e52B6efda2fDe00435f39279` is no longer listed. + +BRIDGE (`0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB`) is a supported token on BSC and Polygon +mainnet: + +- The SDK's chain config lists it under `assets`, with 18 decimals and OpenZeppelin's storage layout + (`balanceSlot` 0, `allowanceSlot` 1). +- Simplex ships `BRIDGE` as a registry symbol, so `[[pairs]]` can name it without an `[assets]` + entry and the setup wizard declares any orderbook market in it. +- The indexer tracks solver inventory in it. It is a `yieldVaults` key with no vaults, so + `SolverInventory` holds the wallet balance only. A solver already tracked on either chain gets its + BRIDGE row at its next daily reconciliation. + +`evm/` now takes interfaces from their official packages instead of declaring its own: + +- `@account-abstraction/contracts@0.9.0-rc.1` for the EntryPoint (`IEntryPoint`, `IStakeManager`). +- `@uniswap/permit2` from `github:Uniswap/permit2#cc56ad0` for `ISignatureTransfer` and + `IAllowanceTransfer`. Permit2's Solidity is not published to npm. + +SimplexPaymaster keeps OpenZeppelin's `IEntryPoint`, which its `PaymasterERC20` base requires. Its +v0.8 calls are explicit casts on the `ENTRYPOINT_V08` address, and `getDepositInfo`, which +OpenZeppelin's interface lacks, goes through the package's `IStakeManager`. + +OpenZeppelin's account bases and the package each declare a `PackedUserOperation` struct, and +Solidity does not convert between them. Tests that send one op to both convert it at the EntryPoint +call with `toEntryPointOp` and `toEntryPointOps` from `evm/tests/foundry/EntryPointOps.sol`. diff --git a/docs/content/developers/evm/contract-addresses/mainnet.mdx b/docs/content/developers/evm/contract-addresses/mainnet.mdx index 73814c399..279bd91de 100644 --- a/docs/content/developers/evm/contract-addresses/mainnet.mdx +++ b/docs/content/developers/evm/contract-addresses/mainnet.mdx @@ -20,10 +20,10 @@ ISMP contract addresses for the following networks: | `TokenGateway (Deprecated)` | [`0xFd413e3AFe560182C4471F4d143A96d3e259B6dE`](https://etherscan.io/address/0xFd413e3AFe560182C4471F4d143A96d3e259B6dE) | | `CallDispatcher` | [`0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd`](https://etherscan.io/address/0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd) | | `IntentGatewayV2` | [`0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716`](https://etherscan.io/address/0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716) | -| `IntentGatewayV2 (Implementation)` | [`0x8E6c939E4915960eDa307c28F1d14840472F2648`](https://etherscan.io/address/0x8E6c939E4915960eDa307c28F1d14840472F2648) | -| `SolverAccount` | [`0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC`](https://etherscan.io/address/0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC) | +| `IntentGatewayV2 (Implementation)` | [`0xF159b72f168bceb982e591176d81ca6568d008Dd`](https://etherscan.io/address/0xF159b72f168bceb982e591176d81ca6568d008Dd) | +| `SolverAccount` | [`0xaAd062555800a97Af062795189e32a3CBd045612`](https://etherscan.io/address/0xaAd062555800a97Af062795189e32a3CBd045612) | | `SimplexPaymaster` | [`0xD4340d7466e040626383cb9cda9307ba8E081149`](https://etherscan.io/address/0xD4340d7466e040626383cb9cda9307ba8E081149) | -| `SimplexPaymaster (Implementation)` | [`0x58F678b5dA7997C7121621495ECFD8984D525e79`](https://etherscan.io/address/0x58F678b5dA7997C7121621495ECFD8984D525e79) | +| `SimplexPaymaster (Implementation)` | [`0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a`](https://etherscan.io/address/0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a) | | `BandwidthManager` | [`0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d`](https://etherscan.io/address/0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d) | | `ConsensusStateId` | `ETH0` | | `StateMachine` | `EVM-1` | @@ -42,10 +42,10 @@ ISMP contract addresses for the following networks: | `TokenGateway (Deprecated)` | [`0xFd413e3AFe560182C4471F4d143A96d3e259B6dE`](https://arbiscan.io/address/0xFd413e3AFe560182C4471F4d143A96d3e259B6dE) | | `CallDispatcher` | [`0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd`](https://arbiscan.io/address/0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd) | | `IntentGatewayV2` | [`0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716`](https://arbiscan.io/address/0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716) | -| `IntentGatewayV2 (Implementation)` | [`0x8E6c939E4915960eDa307c28F1d14840472F2648`](https://arbiscan.io/address/0x8E6c939E4915960eDa307c28F1d14840472F2648) | -| `SolverAccount` | [`0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC`](https://arbiscan.io/address/0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC) | +| `IntentGatewayV2 (Implementation)` | [`0xF159b72f168bceb982e591176d81ca6568d008Dd`](https://arbiscan.io/address/0xF159b72f168bceb982e591176d81ca6568d008Dd) | +| `SolverAccount` | [`0xaAd062555800a97Af062795189e32a3CBd045612`](https://arbiscan.io/address/0xaAd062555800a97Af062795189e32a3CBd045612) | | `SimplexPaymaster` | [`0x7281Bccb4f0BCE44F3B8542d1fC5e51c2F5fC08C`](https://arbiscan.io/address/0x7281Bccb4f0BCE44F3B8542d1fC5e51c2F5fC08C) | -| `SimplexPaymaster (Implementation)` | [`0x58F678b5dA7997C7121621495ECFD8984D525e79`](https://arbiscan.io/address/0x58F678b5dA7997C7121621495ECFD8984D525e79) | +| `SimplexPaymaster (Implementation)` | [`0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a`](https://arbiscan.io/address/0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a) | | `BandwidthManager` | [`0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d`](https://arbiscan.io/address/0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d) | | `ConsensusStateId` | Messaging: `ETH0` / Consensus: `ARB0` | | `StateMachine` | `EVM-42161` | @@ -82,10 +82,10 @@ ISMP contract addresses for the following networks: | `TokenGateway (Deprecated)` | [`0xFd413e3AFe560182C4471F4d143A96d3e259B6dE`](https://basescan.org/address/0xFd413e3AFe560182C4471F4d143A96d3e259B6dE) | | `CallDispatcher` | [`0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd`](https://basescan.org/address/0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd) | | `IntentGatewayV2` | [`0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716`](https://basescan.org/address/0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716) | -| `IntentGatewayV2 (Implementation)` | [`0x8E6c939E4915960eDa307c28F1d14840472F2648`](https://basescan.org/address/0x8E6c939E4915960eDa307c28F1d14840472F2648) | -| `SolverAccount` | [`0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC`](https://basescan.org/address/0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC) | +| `IntentGatewayV2 (Implementation)` | [`0xF159b72f168bceb982e591176d81ca6568d008Dd`](https://basescan.org/address/0xF159b72f168bceb982e591176d81ca6568d008Dd) | +| `SolverAccount` | [`0xaAd062555800a97Af062795189e32a3CBd045612`](https://basescan.org/address/0xaAd062555800a97Af062795189e32a3CBd045612) | | `SimplexPaymaster` | [`0x15b3B03C870c7ef252029c35A12d3b339F5c8d7f`](https://basescan.org/address/0x15b3B03C870c7ef252029c35A12d3b339F5c8d7f) | -| `SimplexPaymaster (Implementation)` | [`0x58F678b5dA7997C7121621495ECFD8984D525e79`](https://basescan.org/address/0x58F678b5dA7997C7121621495ECFD8984D525e79) | +| `SimplexPaymaster (Implementation)` | [`0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a`](https://basescan.org/address/0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a) | | `BandwidthManager` | [`0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d`](https://basescan.org/address/0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d) | | `ConsensusStateId` | Messaging: `ETH0` / Consensus: `BASE` | | `StateMachine` | `EVM-8453` | @@ -103,11 +103,11 @@ ISMP contract addresses for the following networks: | `TokenGateway (Deprecated)` | [`0xFd413e3AFe560182C4471F4d143A96d3e259B6dE`](https://bscscan.com/address/0xFd413e3AFe560182C4471F4d143A96d3e259B6dE) | | `CallDispatcher` | [`0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd`](https://bscscan.com/address/0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd) | | `IntentGatewayV2` | [`0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716`](https://bscscan.com/address/0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716) | -| `IntentGatewayV2 (Implementation)` | [`0x8E6c939E4915960eDa307c28F1d14840472F2648`](https://bscscan.com/address/0x8E6c939E4915960eDa307c28F1d14840472F2648) | -| `SolverAccount` | [`0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC`](https://bscscan.com/address/0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC) | +| `IntentGatewayV2 (Implementation)` | [`0xF159b72f168bceb982e591176d81ca6568d008Dd`](https://bscscan.com/address/0xF159b72f168bceb982e591176d81ca6568d008Dd) | +| `SolverAccount` | [`0xaAd062555800a97Af062795189e32a3CBd045612`](https://bscscan.com/address/0xaAd062555800a97Af062795189e32a3CBd045612) | | `BridgeToken (BRIDGE)` | [`0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB`](https://bscscan.com/address/0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB) | | `SimplexPaymaster` | [`0xeD02f9f0df8F562B89cC5b25867Ad3C2d61252A9`](https://bscscan.com/address/0xeD02f9f0df8F562B89cC5b25867Ad3C2d61252A9) | -| `SimplexPaymaster (Implementation)` | [`0x58F678b5dA7997C7121621495ECFD8984D525e79`](https://bscscan.com/address/0x58F678b5dA7997C7121621495ECFD8984D525e79) | +| `SimplexPaymaster (Implementation)` | [`0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a`](https://bscscan.com/address/0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a) | | `BandwidthManager` | [`0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d`](https://bscscan.com/address/0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d) | | `ConsensusStateId` | `BSC0` | | `StateMachine` | `EVM-56` | @@ -170,11 +170,11 @@ ISMP contract addresses for the following networks: | `TokenGateway (Deprecated)` | [`0x8b536105b6Fae2aE9199f5146D3C57Dfe53b614E`](https://polygonscan.com/address/0x8b536105b6Fae2aE9199f5146D3C57Dfe53b614E) | | `CallDispatcher` | [`0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd`](https://polygonscan.com/address/0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd) | | `IntentGatewayV2` | [`0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716`](https://polygonscan.com/address/0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716) | -| `IntentGatewayV2 (Implementation)` | [`0x8E6c939E4915960eDa307c28F1d14840472F2648`](https://polygonscan.com/address/0x8E6c939E4915960eDa307c28F1d14840472F2648) | -| `SolverAccount` | [`0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC`](https://polygonscan.com/address/0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC) | +| `IntentGatewayV2 (Implementation)` | [`0xF159b72f168bceb982e591176d81ca6568d008Dd`](https://polygonscan.com/address/0xF159b72f168bceb982e591176d81ca6568d008Dd) | +| `SolverAccount` | [`0xaAd062555800a97Af062795189e32a3CBd045612`](https://polygonscan.com/address/0xaAd062555800a97Af062795189e32a3CBd045612) | | `BridgeToken (BRIDGE)` | [`0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB`](https://polygonscan.com/address/0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB) | | `SimplexPaymaster` | [`0xe99acFe0f5fC4C8ea54A187D8D3b05f136150095`](https://polygonscan.com/address/0xe99acFe0f5fC4C8ea54A187D8D3b05f136150095) | -| `SimplexPaymaster (Implementation)` | [`0x58F678b5dA7997C7121621495ECFD8984D525e79`](https://polygonscan.com/address/0x58F678b5dA7997C7121621495ECFD8984D525e79) | +| `SimplexPaymaster (Implementation)` | [`0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a`](https://polygonscan.com/address/0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a) | | `BandwidthManager` | [`0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d`](https://polygonscan.com/address/0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d) | | `ConsensusStateId` | `POLY` | | `StateMachine` | `EVM-137` | @@ -190,7 +190,7 @@ ISMP contract addresses for the following networks: | `FeeToken (USDC)` | [`0x0000053900000000000000000000000001200000`](https://blockscout.polkadot.io/address/0x0000053900000000000000000000000001200000) | | `CallDispatcher` | [`0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd`](https://blockscout.polkadot.io/address/0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd) | | `IntentGatewayV2` | [`0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716`](https://blockscout.polkadot.io/address/0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716) | -| `IntentGatewayV2 (Implementation)` | [`0x8E6c939E4915960eDa307c28F1d14840472F2648`](https://blockscout.polkadot.io/address/0x8E6c939E4915960eDa307c28F1d14840472F2648) | -| `SolverAccount` | [`0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC`](https://blockscout.polkadot.io/address/0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC) | +| `IntentGatewayV2 (Implementation)` | [`0xF159b72f168bceb982e591176d81ca6568d008Dd`](https://blockscout.polkadot.io/address/0xF159b72f168bceb982e591176d81ca6568d008Dd) | +| `SolverAccount` | [`0xaAd062555800a97Af062795189e32a3CBd045612`](https://blockscout.polkadot.io/address/0xaAd062555800a97Af062795189e32a3CBd045612) | | `BandwidthManager` | [`0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d`](https://blockscout.polkadot.io/address/0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d) | | `StateMachine` | `EVM-420420419` | diff --git a/docs/content/developers/sdk/api/simplex.mdx b/docs/content/developers/sdk/api/simplex.mdx index ee397ce27..f4029ab3c 100644 --- a/docs/content/developers/sdk/api/simplex.mdx +++ b/docs/content/developers/sdk/api/simplex.mdx @@ -632,7 +632,7 @@ resolve(symbol: string, chain: string): HexString | null redefined — repointing one under a running market would silently redirect its fills. `resolve` returns `null` when the asset is not deployed or not known on that chain. -The registry ships USDC, USDT, DAI, CNGN, USDR, ZARP, EURC, XSGD and TRYB. +The registry ships USDC, USDT, DAI, CNGN, USDR, ZARP, EURC, XSGD, TRYB and BRIDGE. ## WalletController diff --git a/evm/config.mainnet.toml b/evm/config.mainnet.toml index e0f9a92cc..6064bdc10 100644 --- a/evm/config.mainnet.toml +++ b/evm/config.mainnet.toml @@ -9,7 +9,7 @@ QUOTER = "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -23,10 +23,11 @@ USDC_ORACLE = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6" USDT_TOKEN = "0xdAC17F958D2ee523a2206206994597C13D831ec7" USDT_ORACLE = "0x3E7d1eAB13ad0104d2750B8863b489D65364e32D" SIMPLEX_PAYMASTER = "0xD4340d7466e040626383cb9cda9307ba8E081149" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" +SIMPLEX_PAYMASTER_IMPL = "0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a" [1.uint] MAX_FEE = 500 @@ -47,7 +48,7 @@ WETH = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -55,7 +56,7 @@ CONSENSUS_ROUTER = "0x64094Bb4BCa533C8322a9E6d6ab88e9d2A36909a" BANDWIDTH_MANAGER = "0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d" HANDLER_V2 = "0x2a18AB35DEa43474882E05A661e2F20fe89c0535" SP1_VERIFIER = "0x70303aA3434e13B64F150CE72bad43e13c8eD186" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" NATIVE_ORACLE = "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612" USDC_TOKEN = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" USDC_ORACLE = "0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3" @@ -63,8 +64,9 @@ USDT_TOKEN = "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" USDT_ORACLE = "0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7" SIMPLEX_PAYMASTER = "0x7281Bccb4f0BCE44F3B8542d1fC5e51c2F5fC08C" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" +SIMPLEX_PAYMASTER_IMPL = "0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a" [42161.uint] DEFAULT_FEE = 500 @@ -87,7 +89,7 @@ QUOTER = "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -95,10 +97,10 @@ CONSENSUS_ROUTER = "0x64094Bb4BCa533C8322a9E6d6ab88e9d2A36909a" BANDWIDTH_MANAGER = "0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d" HANDLER_V2 = "0x2a18AB35DEa43474882E05A661e2F20fe89c0535" SP1_VERIFIER = "0x70303aA3434e13B64F150CE72bad43e13c8eD186" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" [10.uint] MAX_FEE = 3000 @@ -119,7 +121,7 @@ WETH = "0x4200000000000000000000000000000000000006" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -133,10 +135,11 @@ USDC_TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" USDC_ORACLE = "0x7e860098F58bBFC8648a4311b374B1D669a2bc6B" USDT_TOKEN = "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2" USDT_ORACLE = "0xf19d560eB8d2ADf07BD6D13ed03e1D11215721F9" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" +SIMPLEX_PAYMASTER_IMPL = "0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a" [8453.uint] DEFAULT_FEE = 3000 @@ -158,7 +161,7 @@ FEE_TOKEN = "0x55d398326f99059fF775485246999027B3197955" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -172,10 +175,11 @@ USDC_TOKEN = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" USDC_ORACLE = "0x51597f405303C4377E36123cBc172b13269EA163" USDT_TOKEN = "0x55d398326f99059fF775485246999027B3197955" USDT_ORACLE = "0xB97Ad0E74fa7d920791E90258A6E2085088b4320" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" +SIMPLEX_PAYMASTER_IMPL = "0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a" [56.bool] is_mainnet = true @@ -190,7 +194,7 @@ FEE_TOKEN = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -198,10 +202,10 @@ CONSENSUS_ROUTER = "0x64094Bb4BCa533C8322a9E6d6ab88e9d2A36909a" BANDWIDTH_MANAGER = "0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d" HANDLER_V2 = "0x2a18AB35DEa43474882E05A661e2F20fe89c0535" SP1_VERIFIER = "0x70303aA3434e13B64F150CE72bad43e13c8eD186" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" [100.bool] is_mainnet = true @@ -224,13 +228,13 @@ V4_QUOTER = "0x3972c00f7ed4885e145823eb7c655375d275a1c5" WETH = "0x4200000000000000000000000000000000000006" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0xd5535d4DeB17F050e52B6efda2fDe00435f39279" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" SP1_VERIFIER = "0x70303aA3434e13B64F150CE72bad43e13c8eD186" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" [1868.uint] DEFAULT_FEE = 3000 @@ -254,7 +258,7 @@ QUOTER = "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" HOST = "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" ECDSA_BEEFY = "0xAeC9508513fF0a7C3c80c9bAc204e572eD2d1233" SP1_BEEFY = "0x3683CD18CDb7C5C223F9489Ac33c9c97EB176cB1" @@ -268,10 +272,11 @@ USDC_ORACLE = "0xfE4A8cc5b5B2366C1B58Bea3858e81843581b2F7" USDT_TOKEN = "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" USDT_ORACLE = "0x0A6513e40db6EB1b165753AD52E80663aeA50545" SIMPLEX_PAYMASTER = "0xe99acFe0f5fC4C8ea54A187D8D3b05f136150095" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" +SIMPLEX_PAYMASTER_IMPL = "0xc2094F7094623fbA8751C58061Dd9Fe26CE5E59a" [137.uint] MAX_FEE = 500 @@ -295,13 +300,13 @@ CONSENSUS_ROUTER = "0x64094Bb4BCa533C8322a9E6d6ab88e9d2A36909a" CALL_DISPATCHER = "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd" BANDWIDTH_MANAGER = "0x6A67533Ce73756FfaB17c05578A5FBBa5d9B2d8d" INTENT_GATEWAY_V2 = "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716" -SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" +SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" PRICE_ORACLE = "0x0000000000000000000000000000000000000000" SP1_VERIFIER = "0x70303aA3434e13B64F150CE72bad43e13c8eD186" -INTENT_GATEWAY_V2_IMPL = "0x8E6c939E4915960eDa307c28F1d14840472F2648" +INTENT_GATEWAY_V2_IMPL = "0xF159b72f168bceb982e591176d81ca6568d008Dd" HOST_MANAGER = "0xc09A8229e2C18dEad6A2795154d7D8C7F1AC5Dfa" -INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x84478d65c814a9180030b2c8090dcCEf213eDF1D" -INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x07691e3B3F1D390dE0b83e232423038F9663CB45" +INTENT_GATEWAY_V2_INTRINSIC_MODULE = "0x022a168d64D85007ED8C7F08A672fd997e0C4bdD" +INTENT_GATEWAY_V2_EXTRINSIC_MODULE = "0x5f925b8870f730B3feb8BdDAEd7Ad088c62a32C2" [420420419.uint] FEE_TOKEN_DECIMALS = 6 diff --git a/evm/package.json b/evm/package.json index ea68b088e..c8c497b60 100644 --- a/evm/package.json +++ b/evm/package.json @@ -12,11 +12,13 @@ "license": "ISC", "description": "", "dependencies": { + "@account-abstraction/contracts": "0.9.0-rc.1", "@hyperbridge/core": "file:../sdk/packages/core", "@openzeppelin/community-contracts": "github:OpenZeppelin/openzeppelin-community-contracts#9e1baed", "@openzeppelin/contracts": "^5.4.0", "@openzeppelin/contracts-upgradeable": "^5.6.1", "@polytope-labs/solidity-merkle-trees": "1.1.0", + "@uniswap/permit2": "github:Uniswap/permit2#cc56ad0", "@uniswap/swap-router-contracts": "^1.3.1", "@uniswap/universal-router": "github:Uniswap/universal-router", "@uniswap/v2-periphery": "^1.1.0-beta.0", diff --git a/evm/pnpm-lock.yaml b/evm/pnpm-lock.yaml index 10bca2e61..33283b239 100644 --- a/evm/pnpm-lock.yaml +++ b/evm/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@account-abstraction/contracts': + specifier: 0.9.0-rc.1 + version: 0.9.0-rc.1 '@hyperbridge/core': specifier: file:../sdk/packages/core version: file:../sdk/packages/core @@ -23,6 +26,9 @@ importers: '@polytope-labs/solidity-merkle-trees': specifier: 1.1.0 version: 1.1.0 + '@uniswap/permit2': + specifier: github:Uniswap/permit2#cc56ad0 + version: https://codeload.github.com/Uniswap/permit2/tar.gz/cc56ad0 '@uniswap/swap-router-contracts': specifier: ^1.3.1 version: 1.3.1(hardhat@2.28.0) @@ -57,6 +63,9 @@ importers: packages: + '@account-abstraction/contracts@0.9.0-rc.1': + resolution: {integrity: sha512-NEii5cuCEJaQ0ZUPAh6JtP/yuozLX4k9BJVu7ytX0QAjc4cT038H0fN0lAO129ou/Uwabj5xY9X+z06ChM3p7A==} + '@ethereumjs/rlp@5.0.2': resolution: {integrity: sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==} engines: {node: '>=18'} @@ -293,6 +302,10 @@ packages: resolution: {integrity: sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==} engines: {node: '>=10'} + '@uniswap/permit2@https://codeload.github.com/Uniswap/permit2/tar.gz/cc56ad0': + resolution: {tarball: https://codeload.github.com/Uniswap/permit2/tar.gz/cc56ad0} + version: 1.0.0 + '@uniswap/swap-router-contracts@1.3.1': resolution: {integrity: sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg==} engines: {node: '>=10'} @@ -988,6 +1001,11 @@ packages: snapshots: + '@account-abstraction/contracts@0.9.0-rc.1': + dependencies: + '@openzeppelin/contracts': 5.4.0 + '@uniswap/v3-periphery': 1.4.4 + '@ethereumjs/rlp@5.0.2': {} '@ethereumjs/util@9.1.0': @@ -1302,6 +1320,8 @@ snapshots: '@uniswap/lib@4.0.1-alpha': {} + '@uniswap/permit2@https://codeload.github.com/Uniswap/permit2/tar.gz/cc56ad0': {} + '@uniswap/swap-router-contracts@1.3.1(hardhat@2.28.0)': dependencies: '@openzeppelin/contracts': 3.4.2-solc-0.7 diff --git a/evm/remappings.txt b/evm/remappings.txt index 016b0a90d..71c2038ad 100644 --- a/evm/remappings.txt +++ b/evm/remappings.txt @@ -1,3 +1,4 @@ +@account-abstraction/=node_modules/@account-abstraction/ @hyperbridge/core/=node_modules/@hyperbridge/core/contracts/ @openzeppelin/=node_modules/@openzeppelin/ @polytope-labs/=node_modules/@polytope-labs/ diff --git a/evm/script/IntentGatewayScript.sol b/evm/script/IntentGatewayScript.sol index 779de64ac..0590339e4 100644 --- a/evm/script/IntentGatewayScript.sol +++ b/evm/script/IntentGatewayScript.sol @@ -10,14 +10,13 @@ import {IntrinsicModule} from "../src/apps/intentsv2/IntrinsicModule.sol"; import {ExtrinsicModule} from "../src/apps/intentsv2/ExtrinsicModule.sol"; import {BaseScript} from "./BaseScript.sol"; -/// @dev Initialization payload for an upgrade of an existing proxy: `migrate(owner)` for a proxy -/// at 2, nothing for one already at 3. -function intentGatewayUpgradeInitialization(IntentGatewayV2 gateway, address owner) view returns (bytes memory) { +/// @dev Initialization payload for an upgrade of an existing proxy: `migrate()` for a proxy at 3, +/// nothing for one already at 4. +function intentGatewayUpgradeInitialization(IntentGatewayV2 gateway) view returns (bytes memory) { uint64 current = gateway.version(); - if (current == 3) return bytes(""); - require(current == 2, "Unsupported IntentGateway version"); - require(owner != address(0), "GATEWAY_OWNER is unset"); - return abi.encodeCall(IntentGatewayV2.migrate, (owner)); + if (current == 4) return bytes(""); + require(current == 3, "Unsupported IntentGateway version"); + return abi.encodeCall(IntentGatewayV2.migrate, ()); } /// @notice Shared by the IntentGatewayV2 deploy scripts: modules first, then the implementation. @@ -30,15 +29,13 @@ abstract contract IntentGatewayScript is BaseScript { */ function _deployImplementation() internal returns (IntentGatewayV2 implementation) { // Query the configured proxy before broadcasting any deployments. New chains initialize - // their fresh proxy separately; an existing v3 proxy must not re-run migrate(). + // their fresh proxy separately; an existing v4 proxy must not re-run migrate(). vm.stopBroadcast(); bool hasProxy = config.exists("INTENT_GATEWAY_V2"); bytes memory migration; if (hasProxy) { - migration = intentGatewayUpgradeInitialization( - IntentGatewayV2(payable(config.get("INTENT_GATEWAY_V2").toAddress())), - vm.envOr("GATEWAY_OWNER", address(0)) - ); + IntentGatewayV2 proxy = IntentGatewayV2(payable(config.get("INTENT_GATEWAY_V2").toAddress())); + migration = intentGatewayUpgradeInitialization(proxy); } vm.startBroadcast(uint256(privateKey)); address intrinsic = vm.computeCreate2Address(salt, keccak256(type(IntrinsicModule).creationCode)); diff --git a/evm/src/apps/IntentGatewayV2.sol b/evm/src/apps/IntentGatewayV2.sol index cc61328da..1e1fe0659 100644 --- a/evm/src/apps/IntentGatewayV2.sol +++ b/evm/src/apps/IntentGatewayV2.sol @@ -30,6 +30,7 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/Pau import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IUniswapV2Router02} from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { TokenInfo, Order, @@ -69,8 +70,9 @@ contract IntentGatewayV2 is address public immutable extrinsicModule; /// @dev The `Initializable` version this implementation lands a proxy on, through `initialize` - /// or `migrate`. 3 is the module split, the owner and solver quotes, which land together. - uint64 private constant VERSION = 3; + /// or `migrate`. 3 is the module split, the owner and solver quotes, which land together. 4 binds + /// solver selection to the EntryPoint v0.9 UserOperation; its storage is unchanged. + uint64 private constant VERSION = 4; /** * @dev Records the modules and locks this implementation against initialization. Modules must @@ -125,6 +127,14 @@ contract IntentGatewayV2 is _; } + /** + * @dev `migrate` only takes a proxy one version behind; an older one would skip a migration. + */ + modifier onlyPreviousVersion() { + if (_getInitializedVersion() != VERSION - 1) revert InvalidInitialization(); + _; + } + /** * @dev Initializes a bare proxy with its peers, params, relayer and owner. */ @@ -143,17 +153,10 @@ contract IntentGatewayV2 is } /** - * @dev Takes a version-2 proxy to `VERSION`, as the init data of its upgrade. Moves `_relayer` - * from slot 13 offset 1 to offset 0, dropping the removed `_paused` byte, and sets the owner. + * @dev Takes a version-3 proxy to `VERSION`, as the init data of its upgrade. Storage is + * unchanged, so it only records the version. */ - function migrate(address owner_) external onlyHost reinitializer(VERSION) { - assembly ("memory-safe") { - sstore(_relayer.slot, shr(8, sload(_relayer.slot))) - } - __Ownable_init(owner_); - __Ownable2Step_init(); - __Pausable_init(); - } + function migrate() external onlyHost onlyPreviousVersion reinitializer(VERSION) {} /** * @dev Also accepts the host, so governance can pause, resume or replace the owner through @@ -433,7 +436,7 @@ contract IntentGatewayV2 is if (_params.solverSelection) { // Only inside the UserOperation the session key selected. Zero means no UserOperation // is executing, as for a direct call. - bytes32 userOpHash = ENTRYPOINT_V09.getCurrentUserOpHash(); + bytes32 userOpHash = IEntryPoint(entrypoint()).getCurrentUserOpHash(); if (userOpHash == bytes32(0)) revert Unauthorized(); // Each (UserOperation, signer) pair has its own slot, so no other selection in the same diff --git a/evm/src/apps/intentsv2/IntentsBase.sol b/evm/src/apps/intentsv2/IntentsBase.sol index 11976cc10..7ec572d48 100644 --- a/evm/src/apps/intentsv2/IntentsBase.sol +++ b/evm/src/apps/intentsv2/IntentsBase.sol @@ -44,14 +44,6 @@ interface IArbSys { function arbBlockNumber() external view returns (uint256); } -/** - * @dev The one ERC-4337 EntryPoint v0.9 view the gateway reads: the hash of the UserOperation - * being executed, zero outside an execution. - */ -interface IEntryPointV09 { - function getCurrentUserOpHash() external view returns (bytes32); -} - /** * @title IntentsBase * @author Polytope Labs (hello@polytope.technology) @@ -73,7 +65,7 @@ abstract contract IntentsBase is EIP712 { * Fixed rather than taken from the caller: anyone may call the gateway, so a caller posing as * an EntryPoint could report whatever hash a selection was signed for. */ - IEntryPointV09 internal constant ENTRYPOINT_V09 = IEntryPointV09(0x433709009B8330FDa32311DF1C2AFA402eD8D009); + address internal constant ENTRYPOINT_V09 = 0x433709009B8330FDa32311DF1C2AFA402eD8D009; /** * @dev Sentinel key under which the Hyperbridge relayer fees are held in `_orders`. The low @@ -192,7 +184,7 @@ abstract contract IntentsBase is EIP712 { /** * @dev Once set, the only relayer whose deliveries `onAccept` and `onGetResponse` accept. Slot 13 * offset 0. Earlier implementations packed it at offset 1, behind an unused `bool _paused` that - * has since been removed; `IntentGatewayV2.migrate` moves it. + * has since been removed; the version-3 `migrate` moved it. */ address internal _relayer; @@ -454,6 +446,13 @@ abstract contract IntentsBase is EIP712 { return _relayer; } + /** + * @notice The ERC-4337 EntryPoint that reports the UserOperation a selected fill runs in. + */ + function entrypoint() public pure returns (address) { + return ENTRYPOINT_V09; + } + /** * @dev The block number order deadlines use. On Arbitrum that is the L2 block from ArbSys; * checking the chain id keeps the bytecode, and so the CREATE2 address, the same on every @@ -484,19 +483,17 @@ abstract contract IntentsBase is EIP712 { } /** - * @dev Checks every leg's shape, skipped and completed legs included, before any transfer. + * @dev Checks leg `i`'s shape. Runs for skipped and completed legs too. */ - function _validateLegs(Order calldata order, FillOptions calldata options) private pure { - for (uint256 i; i < order.output.assets.length; ++i) { - // A token is the address in its low 20 bytes; anything above would let one token pass - // `_isRepeatedToken` as two. Checked here too: a cross-chain fill never sees `placeOrder`. - if (uint256(order.inputs[i].token) >> 160 != 0) revert InvalidInput(); - if (uint256(order.output.assets[i].token) >> 160 != 0) revert InvalidInput(); - if (options.inputs[i].token != order.inputs[i].token) revert InvalidInput(); - if (options.outputs[i].token != order.output.assets[i].token) revert InvalidInput(); - // A leg is skipped by quoting zero on both sides, never on one. - if ((options.inputs[i].amount == 0) != (options.outputs[i].amount == 0)) revert InvalidInput(); - } + function _validateLeg(Order calldata order, FillOptions calldata options, uint256 i) private pure { + // A token is the address in its low 20 bytes; anything above would let one token pass + // `_isRepeatedToken` as two. Checked here too: a cross-chain fill never sees `placeOrder`. + if (uint256(order.inputs[i].token) >> 160 != 0) revert InvalidInput(); + if (uint256(order.output.assets[i].token) >> 160 != 0) revert InvalidInput(); + if (options.inputs[i].token != order.inputs[i].token) revert InvalidInput(); + if (options.outputs[i].token != order.output.assets[i].token) revert InvalidInput(); + // A leg is skipped by quoting zero on both sides, never on one. + if ((options.inputs[i].amount == 0) != (options.outputs[i].amount == 0)) revert InvalidInput(); } /** @@ -506,7 +503,6 @@ abstract contract IntentsBase is EIP712 { internal returns (FillResult memory result) { - _validateLegs(order, options); uint256 legCount = order.output.assets.length; result.releasedInputs = new TokenInfo[](legCount); result.creditedOutputs = new TokenInfo[](legCount); @@ -515,6 +511,7 @@ abstract contract IntentsBase is EIP712 { bool madeProgress; for (uint256 i; i < legCount; ++i) { + _validateLeg(order, options, i); madeProgress = _fillLeg(order, options, commitment, i, result) || madeProgress; } diff --git a/evm/src/utils/SimplexPaymaster.sol b/evm/src/utils/SimplexPaymaster.sol index c585ee18d..d0044e111 100644 --- a/evm/src/utils/SimplexPaymaster.sol +++ b/evm/src/utils/SimplexPaymaster.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IStakeManager} from "@account-abstraction/contracts/interfaces/IStakeManager.sol"; import {PaymasterERC20} from "@openzeppelin/community-contracts/contracts/account/paymaster/PaymasterERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -15,6 +16,7 @@ import {HyperApp} from "@hyperbridge/core/apps/HyperApp.sol"; import {IncomingPostRequest} from "@hyperbridge/core/interfaces/IApp.sol"; import {IDispatcher} from "@hyperbridge/core/interfaces/IDispatcher.sol"; import {IUniswapV2Router02} from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; +import {ISignatureTransfer} from "@uniswap/permit2/src/interfaces/ISignatureTransfer.sol"; /// @notice Minimal Chainlink AggregatorV3 interface — no external dependency needed. interface AggregatorV3Interface { @@ -26,45 +28,6 @@ interface AggregatorV3Interface { function decimals() external view returns (uint8); } -/// @notice Minimal Permit2 SignatureTransfer interface — no external dependency needed. -interface ISignatureTransfer { - struct TokenPermissions { - address token; - uint256 amount; - } - - struct PermitTransferFrom { - TokenPermissions permitted; - uint256 nonce; - uint256 deadline; - } - - struct SignatureTransferDetails { - address to; - uint256 requestedAmount; - } - - function permitTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes calldata signature - ) external; -} - -/// @notice The EntryPoint's `getDepositInfo` view, which OpenZeppelin's IEntryPointStake omits. -interface IStakeManager { - struct DepositInfo { - uint256 deposit; - bool staked; - uint112 stake; - uint32 unstakeDelaySec; - uint48 withdrawTime; - } - - function getDepositInfo(address account) external view returns (DepositInfo memory info); -} - /// @title SimplexPaymaster /// @author Polytope Labs /// @notice Fully onchain, permissionless ERC-4337 v0.9 paymaster that accepts @@ -211,10 +174,10 @@ contract SimplexPaymaster is Initializable, HyperApp, PaymasterERC20 { uint256 internal constant PERMIT2_DATA_LENGTH = 182; /// @dev EntryPoint v0.9, the only one this paymaster serves. - IEntryPoint private constant ENTRYPOINT_V09 = IEntryPoint(0x433709009B8330FDa32311DF1C2AFA402eD8D009); + address private constant ENTRYPOINT_V09 = 0x433709009B8330FDa32311DF1C2AFA402eD8D009; /// @dev EntryPoint v0.8, which {migrate} drains and {withdrawStakeV08} unstakes. - IEntryPoint private constant ENTRYPOINT_V08 = ERC4337Utils.ENTRYPOINT_V08; + address private constant ENTRYPOINT_V08 = address(ERC4337Utils.ENTRYPOINT_V08); /// @notice The local Hyperbridge host; the only address allowed to deliver /// governance requests. @@ -328,9 +291,9 @@ contract SimplexPaymaster is Initializable, HyperApp, PaymasterERC20 { /// {withdrawStakeV08} moves the unlocked v0.8 stake into the v0.9 deposit once its delay /// passes. function migrate() external onlyHost onlyPreviousVersion reinitializer(VERSION) { - IStakeManager.DepositInfo memory info = IStakeManager(address(ENTRYPOINT_V08)).getDepositInfo(address(this)); - if (info.deposit > 0) ENTRYPOINT_V08.withdrawTo(payable(address(this)), info.deposit); - if (info.staked) ENTRYPOINT_V08.unlockStake(); + IStakeManager.DepositInfo memory info = IStakeManager(ENTRYPOINT_V08).getDepositInfo(address(this)); + if (info.deposit > 0) IEntryPoint(ENTRYPOINT_V08).withdrawTo(payable(address(this)), info.deposit); + if (info.staked) IEntryPoint(ENTRYPOINT_V08).unlockStake(); uint256 deposited = address(this).balance; if (deposited > 0) entryPoint().depositTo{value: deposited}(address(this)); @@ -342,7 +305,7 @@ contract SimplexPaymaster is Initializable, HyperApp, PaymasterERC20 { /// @dev Permissionless: the native only ever lands in this paymaster's own v0.9 deposit, v0.8 /// enforces the unstake delay and pays out once, and ERC-20 prefunds do not move. function withdrawStakeV08() external { - ENTRYPOINT_V08.withdrawStake(payable(address(this))); + IEntryPoint(ENTRYPOINT_V08).withdrawStake(payable(address(this))); uint256 balance = address(this).balance; if (balance > 0) entryPoint().depositTo{value: balance}(address(this)); } @@ -351,7 +314,7 @@ contract SimplexPaymaster is Initializable, HyperApp, PaymasterERC20 { /// receives every deposit, stake and withdrawal call; only {migrate} and /// {withdrawStakeV08} reach v0.8. function entryPoint() public pure override returns (IEntryPoint) { - return ENTRYPOINT_V09; + return IEntryPoint(ENTRYPOINT_V09); } /// @notice The `Initializable` version: 0 on a bare proxy, `VERSION` once `initialize` or diff --git a/evm/tests/foundry/EntryPointOps.sol b/evm/tests/foundry/EntryPointOps.sol new file mode 100644 index 000000000..700b0b08b --- /dev/null +++ b/evm/tests/foundry/EntryPointOps.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {PackedUserOperation as EntryPointUserOp} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +/** + * @dev `op` as the EntryPoint interface's own struct. OpenZeppelin's account and paymaster bases + * declare an identical one, and Solidity never converts between two struct types. + */ +function toEntryPointOp(PackedUserOperation memory op) pure returns (EntryPointUserOp memory) { + return abi.decode(abi.encode(op), (EntryPointUserOp)); +} + +/** + * @dev `ops` as the EntryPoint interface's own struct, for `handleOps`. + */ +function toEntryPointOps(PackedUserOperation[] memory ops) pure returns (EntryPointUserOp[] memory) { + return abi.decode(abi.encode(ops), (EntryPointUserOp[])); +} diff --git a/evm/tests/foundry/IntentGatewayModulesTest.sol b/evm/tests/foundry/IntentGatewayModulesTest.sol index 34498e330..31b9d1788 100644 --- a/evm/tests/foundry/IntentGatewayModulesTest.sol +++ b/evm/tests/foundry/IntentGatewayModulesTest.sol @@ -345,66 +345,55 @@ contract IntentGatewayModulesTest is MainnetForkBaseTest { /// @dev OpenZeppelin's `Initializable` namespaced slot; `_initialized` is its low 8 bytes. bytes32 internal constant INITIALIZABLE_SLOT = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - /// The release's own upgrade: a proxy at version 2, as every live one is, moved to this - /// implementation with `migrate(owner)` as the init data lands at 4 with its state intact and - /// its owner set. Pinned here because the live-fork test's precondition expires once mainnet - /// is upgraded. - function testUpgradeFromVersionTwoWithMigrate() public { + /// The release's own upgrade: a proxy at version 3, as every live one is, moved to this + /// implementation with `migrate()` as the init data lands at 4 with its state intact. Pinned + /// here because the live-fork test's precondition expires once mainnet is upgraded. + function testUpgradeFromVersionThreeWithMigrate() public { Order memory order = _placeSameChainOrder(1000 * 1e6, 900 * 1e18); - vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(2))); - assertEq(gateway.version(), 2, "a proxy on the previous implementation"); + vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(3))); + assertEq(gateway.version(), 3, "a proxy on the previous implementation"); + address owner = gateway.owner(); IntentGatewayV2 newImpl = deployIntentGatewayImpl(); - PostRequest memory upgrade = - _upgradeRequest(address(newImpl), abi.encodeCall(IntentGatewayV2.migrate, (address(this)))); + PostRequest memory upgrade = _upgradeRequest(address(newImpl), abi.encodeCall(IntentGatewayV2.migrate, ())); vm.expectEmit(true, true, true, true, address(gateway)); - emit Initializable.Initialized(3); + emit Initializable.Initialized(4); vm.prank(address(host)); gateway.onAccept(IncomingPostRequest({relayer: address(this), request: upgrade})); - assertEq(gateway.version(), 3, "migrated"); - assertEq(gateway.owner(), address(this), "owner set by the migration"); + assertEq(gateway.version(), 4, "migrated"); + assertEq(gateway.owner(), owner, "owner survives"); assertEq(gateway._orders(keccak256(abi.encode(order)), 0), 1000 * 1e6, "escrow survives"); assertEq(gateway.instance(bytes("DEST_CHAIN")), address(gateway), "peers survive"); // `migrate` is one-shot: a second upgrade carrying it is refused. - PostRequest memory again = _upgradeRequest( - address(deployIntentGatewayImpl()), abi.encodeCall(IntentGatewayV2.migrate, (address(this))) - ); + PostRequest memory again = + _upgradeRequest(address(deployIntentGatewayImpl()), abi.encodeCall(IntentGatewayV2.migrate, ())); vm.prank(address(host)); vm.expectRevert(Initializable.InvalidInitialization.selector); gateway.onAccept(IncomingPostRequest({relayer: address(this), request: again})); } function testUpgradeHelperUsesEmptyInitializationForVersionFour() public view { - assertEq(intentGatewayUpgradeInitialization(gateway, address(this)), bytes("")); - } - - function testUpgradeHelperMigratesVersionTwo() public { - vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(2))); - assertEq( - intentGatewayUpgradeInitialization(gateway, address(this)), - abi.encodeCall(IntentGatewayV2.migrate, (address(this))) - ); + assertEq(intentGatewayUpgradeInitialization(gateway), bytes("")); } - function testUpgradeHelperRequiresAnOwnerToMigrate() public { - vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(2))); - vm.expectRevert("GATEWAY_OWNER is unset"); - this.upgradeInitialization(address(gateway), address(0)); + function testUpgradeHelperMigratesVersionThree() public { + vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(3))); + assertEq(intentGatewayUpgradeInitialization(gateway), abi.encodeCall(IntentGatewayV2.migrate, ())); } function testUpgradeHelperRejectsUnsupportedVersions() public { - uint256[3] memory unsupported = [uint256(0), uint256(1), uint256(5)]; + uint256[4] memory unsupported = [uint256(0), uint256(1), uint256(2), uint256(5)]; for (uint256 i; i < unsupported.length; i++) { vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(unsupported[i])); vm.expectRevert("Unsupported IntentGateway version"); - this.upgradeInitialization(address(gateway), address(this)); + this.upgradeInitialization(address(gateway)); } } - function upgradeInitialization(address target, address owner) external view returns (bytes memory) { - return intentGatewayUpgradeInitialization(IntentGatewayV2(payable(target)), owner); + function upgradeInitialization(address target) external view returns (bytes memory) { + return intentGatewayUpgradeInitialization(IntentGatewayV2(payable(target))); } /// Upgrading is still possible after the split, and repeatedly. The second upgrade is the one @@ -421,7 +410,7 @@ contract IntentGatewayModulesTest is MainnetForkBaseTest { assertEq(_implementationOf(address(gateway)), address(next), "implementation installed"); assertEq(gateway.intrinsicModule(), next.intrinsicModule(), "modules follow the implementation"); assertEq(gateway.extrinsicModule(), next.extrinsicModule()); - assertEq(gateway.version(), 3, "no migration ran"); + assertEq(gateway.version(), 4, "no migration ran"); assertEq(gateway._nonce(), 1, "_nonce preserved"); assertEq(gateway._orders(commitment, 0), 1000 * 1e6, "escrow preserved"); assertEq(gateway.instance(bytes("DEST_CHAIN")), address(gateway), "peers preserved"); diff --git a/evm/tests/foundry/IntentGatewayProtocolFeesTest.sol b/evm/tests/foundry/IntentGatewayProtocolFeesTest.sol index f763db920..15427c1c5 100644 --- a/evm/tests/foundry/IntentGatewayProtocolFeesTest.sol +++ b/evm/tests/foundry/IntentGatewayProtocolFeesTest.sol @@ -413,7 +413,7 @@ contract IntentGatewayProtocolFeesTest is MainnetForkBaseTest { abi.encodeCall(ExtrinsicIntents.upgradeToAndCall, (address(deployIntentGatewayImpl()), bytes(""))), true ); - assertEq(gateway.version(), 3); + assertEq(gateway.version(), 4); assertEq(gateway._orders(commitment, 0), 900); uint256 before = usdc.balanceOf(user); vm.recordLogs(); diff --git a/evm/tests/foundry/IntentGatewayV2MultiLegTest.sol b/evm/tests/foundry/IntentGatewayV2MultiLegTest.sol index 97c0d5abb..2ca3f6749 100644 --- a/evm/tests/foundry/IntentGatewayV2MultiLegTest.sol +++ b/evm/tests/foundry/IntentGatewayV2MultiLegTest.sol @@ -308,13 +308,14 @@ contract IntentGatewayV2MultiLegTest is MainnetForkBaseTest { assertEq(gateway._filled(commitment), solverB); } - function testFill_ValidatesLaterLegBeforeTokenTransfer() public { + function testFill_InvalidLaterLegRevertsWholeFill() public { (Order memory sameChain, bytes32 commitment) = _place(gateway, _ladder("", host.host())); Order memory crossChain = _ladder(bytes("SOURCE_CHAIN"), host.host()); TokenInfo[] memory takes = _legs([usdcToken, usdcToken], [uint256(1200 * 1e6), 1000 * 1e6]); TokenInfo[] memory outputs = _legs([daiToken, daiToken], [uint256(1200 * 1e18), 990 * 1e18]); outputs[1].token = usdcToken; - vm.mockCallRevert(address(dai), abi.encodeWithSelector(IERC20.transferFrom.selector), hex"deadbeef"); + uint256 solverDai = dai.balanceOf(solverA); + uint256 userDai = dai.balanceOf(user); vm.expectRevert(IntentsBase.InvalidInput.selector); vm.prank(solverA); @@ -323,10 +324,11 @@ contract IntentGatewayV2MultiLegTest is MainnetForkBaseTest { vm.prank(solverA); gateway.fillOrder(crossChain, FillOptions(0, 0, 0, outputs, takes)); - vm.clearMockedCalls(); assertEq(gateway._partialFills(commitment, 0), 0); assertEq(gateway._orders(commitment, 0), 1200 * 1e6); assertEq(gateway._filled(commitment), address(0)); + assertEq(dai.balanceOf(solverA), solverDai); + assertEq(dai.balanceOf(user), userDai); } function testRate_CompletedAndSkippedLegsStillValidateQuotes() public { diff --git a/evm/tests/foundry/IntentGatewayV2Test.sol b/evm/tests/foundry/IntentGatewayV2Test.sol index 0fe8a2d9d..76a1dbe87 100644 --- a/evm/tests/foundry/IntentGatewayV2Test.sol +++ b/evm/tests/foundry/IntentGatewayV2Test.sol @@ -35,7 +35,9 @@ import { SelectOptions } from "@hyperbridge/core/apps/IntentGatewayV2.sol"; import {deployIntentGatewayImpl, deployIntentModules} from "./IntentGatewayDeploy.sol"; -import {IntentsBase, IEntryPointV09} from "../../src/apps/intentsv2/IntentsBase.sol"; +import {intentGatewayUpgradeInitialization} from "../../script/IntentGatewayScript.sol"; +import {IntentsBase} from "../../src/apps/intentsv2/IntentsBase.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {ExtrinsicIntents} from "../../src/apps/intentsv2/ExtrinsicIntents.sol"; import {HyperApp} from "@hyperbridge/core/apps/HyperApp.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -1735,7 +1737,8 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { Order memory order = _placeSelectionOrder(gatewayWithSelection); _selectUserOp(gatewayWithSelection, order, BID_USER_OP_HASH); - assertEq(IEntryPointV09(ENTRYPOINT_V09).getCurrentUserOpHash(), bytes32(0)); + assertEq(gatewayWithSelection.entrypoint(), ENTRYPOINT_V09); + assertEq(IEntryPoint(ENTRYPOINT_V09).getCurrentUserOpHash(), bytes32(0)); FillOptions memory options = _fullFillOptions(order); vm.prank(filler); @@ -1759,7 +1762,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { ); assertEq(recovered, order.session, "the session key selects the zero hash"); - assertEq(IEntryPointV09(ENTRYPOINT_V09).getCurrentUserOpHash(), bytes32(0)); + assertEq(IEntryPoint(ENTRYPOINT_V09).getCurrentUserOpHash(), bytes32(0)); FillOptions memory options = _fullFillOptions(order); vm.prank(filler); @@ -1814,7 +1817,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { /// where the EntryPoint would fail. function testFillOrderWithoutSolverSelectionIgnoresEntryPoint() public { Order memory order = _placeSelectionOrder(intentGateway); - vm.mockCallRevert(ENTRYPOINT_V09, abi.encodeCall(IEntryPointV09.getCurrentUserOpHash, ()), "no EntryPoint here"); + vm.mockCallRevert(ENTRYPOINT_V09, abi.encodeCall(IEntryPoint.getCurrentUserOpHash, ()), "no EntryPoint here"); FillOptions memory options = _fullFillOptions(order); vm.prank(filler); @@ -1906,7 +1909,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { /// @dev Has the EntryPoint report `userOpHash` as the UserOperation it is executing. function _mockCurrentUserOp(bytes32 userOpHash) internal { - vm.mockCall(ENTRYPOINT_V09, abi.encodeCall(IEntryPointV09.getCurrentUserOpHash, ()), abi.encode(userOpHash)); + vm.mockCall(ENTRYPOINT_V09, abi.encodeCall(IEntryPoint.getCurrentUserOpHash, ()), abi.encode(userOpHash)); } /// @dev A quote for all of a `_placeSelectionOrder` order at its own rate. @@ -4214,7 +4217,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { intentGateway.onAccept(IncomingPostRequest({relayer: relayer, request: request})); assertEq(intentGateway.relayer(), next); - assertEq(intentGateway.version(), 3, "no migration ran"); + assertEq(intentGateway.version(), 4, "no migration ran"); assertEq(_implementationOf(address(intentGateway)), implBefore, "implementation unchanged"); } @@ -4231,7 +4234,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { /// Here `migrate` on a proxy already at `VERSION`, three delegatecalls deep. function testExecuteBubblesReverts() public { PostRequest memory request = _upgradeRequest( - host.hyperbridge(), address(_upgradedImpl()), abi.encodeCall(IntentGatewayV2.migrate, (address(this))) + host.hyperbridge(), address(_upgradedImpl()), abi.encodeCall(IntentGatewayV2.migrate, ()) ); vm.prank(address(host)); vm.expectRevert(Initializable.InvalidInitialization.selector); @@ -4325,7 +4328,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { assertEq(gateway.params().host, address(host), "params set via atomic init"); assertEq(gateway.instance(bytes("SOURCE_CHAIN")), address(gateway), "peer bound to address(this)"); assertEq(gateway.relayer(), relayer, "relayer armed from the init data"); - assertEq(gateway.version(), 3, "at VERSION from the init data"); + assertEq(gateway.version(), 4, "at VERSION from the init data"); vm.expectRevert(); gateway.initialize( @@ -4494,17 +4497,11 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { return bytes32(uint256(uint160(r))); } - /// @dev Slot 13 as earlier implementations left it: an unset `bool _paused` at offset 0 and - /// `_relayer` packed behind it at offset 1. - function _legacyRelayerSlot(address r) internal pure returns (bytes32) { - return bytes32(uint256(uint160(r)) << 8); - } - /// 0 on a bare proxy, 2 after `initialize`; the raw implementation is locked at the maximum. function testVersionTracksInitialization() public { IntentGatewayV2 bare = _deployGatewayProxy(); assertEq(bare.version(), 0, "bare proxy"); - assertEq(intentGateway.version(), 3, "initialized"); + assertEq(intentGateway.version(), 4, "initialized"); address impl = _implementationOf(address(intentGateway)); assertEq(IntentGatewayV2(payable(impl)).version(), type(uint64).max, "raw implementation is locked"); } @@ -4548,7 +4545,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { vm.prank(address(host)); intentGateway.onAccept(IncomingPostRequest({relayer: relayer, request: rotate})); assertEq(intentGateway.relayer(), next); - assertEq(intentGateway.version(), 3, "a rotation is not a migration"); + assertEq(intentGateway.version(), 4, "a rotation is not a migration"); // The previous relayer is locked out immediately. vm.prank(address(host)); @@ -4565,8 +4562,8 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { function testMigrateRunsOnce() public { vm.prank(address(host)); vm.expectRevert(Initializable.InvalidInitialization.selector); - intentGateway.migrate(address(this)); - assertEq(intentGateway.version(), 3, "version unchanged"); + intentGateway.migrate(); + assertEq(intentGateway.version(), 4, "version unchanged"); } /// A proxy an upgrade left at an earlier version cannot be re-initialized by anyone; only the @@ -4580,42 +4577,51 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { vm.prank(user); vm.expectRevert(Initializable.InvalidInitialization.selector); gateway.initialize(InitParams({params: p, peerChains: new bytes[](0), relayer: user, owner: address(this)})); - assertEq(gateway.version(), 2, "still at version 2"); + assertEq(gateway.version(), 3, "still at version 3"); vm.prank(address(host)); - gateway.migrate(address(this)); - assertEq(gateway.version(), 3); + gateway.migrate(); + assertEq(gateway.version(), 4); } function testMigrateRejectsEveryoneButHost() public { IntentGatewayV2 gateway = _legacyGateway(); vm.expectRevert(HyperApp.UnauthorizedCall.selector); - gateway.migrate(address(this)); + gateway.migrate(); vm.prank(user); vm.expectRevert(HyperApp.UnauthorizedCall.selector); - gateway.migrate(address(this)); + gateway.migrate(); - assertEq(gateway.version(), 2, "still at version 2"); + assertEq(gateway.version(), 3, "still at version 3"); } - /// `migrate` moves the relayer from slot 13 offset 1 to offset 0, dropping the removed `_paused` - /// byte ahead of it, and bumps the version. The relayer keeps its value, so the gate is unchanged. - function testMigrateMovesTheRelayerToOffsetZero() public { + /// `migrate` only bumps the version: the relayer slot and the owner are as the proxy had them. + function testMigrateOnlyBumpsTheVersion() public { IntentGatewayV2 gateway = _legacyGateway(); - // As an earlier implementation left it, with the old `_paused` byte set to show it is dropped. - vm.store(address(gateway), bytes32(uint256(13)), bytes32(uint256(_legacyRelayerSlot(relayer)) | 1)); + bytes32 relayerSlot = vm.load(address(gateway), bytes32(uint256(13))); + address owner = gateway.owner(); vm.expectEmit(true, true, true, true, address(gateway)); - emit Initializable.Initialized(3); + emit Initializable.Initialized(4); vm.prank(address(host)); - gateway.migrate(address(this)); + gateway.migrate(); - assertEq(gateway.relayer(), relayer, "relayer moved"); - assertEq(vm.load(address(gateway), bytes32(uint256(13))), _relayerSlot(relayer), "old _paused byte dropped"); - assertFalse(gateway.paused(), "the old byte does not pause"); - assertEq(gateway.version(), 3); + assertEq(vm.load(address(gateway), bytes32(uint256(13))), relayerSlot, "relayer slot untouched"); + assertEq(gateway.owner(), owner, "owner unchanged"); + assertEq(gateway.version(), 4); + } + + /// A proxy two versions behind would skip a migration, so `migrate` refuses it. + function testMigrateRefusesOlderVersions() public { + IntentGatewayV2 gateway = _legacyGateway(); + vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(2))); + + vm.prank(address(host)); + vm.expectRevert(Initializable.InvalidInitialization.selector); + gateway.migrate(); + assertEq(gateway.version(), 2, "still at version 2"); } /// `initialize` arms the gate from the init data and lands at `VERSION`. @@ -4625,20 +4631,20 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { vm.expectEmit(true, true, true, true, address(gateway)); emit IntentsBase.RelayerUpdated(address(0), relayer); vm.expectEmit(true, true, true, true, address(gateway)); - emit Initializable.Initialized(3); + emit Initializable.Initialized(4); gateway.initialize(InitParams({params: p, peerChains: new bytes[](0), relayer: relayer, owner: address(this)})); assertEq(gateway.relayer(), relayer); - assertEq(gateway.version(), 3); + assertEq(gateway.version(), 4); } /// @dev OpenZeppelin's `Initializable` namespaced slot; `_initialized` is its low 8 bytes. bytes32 internal constant INITIALIZABLE_SLOT = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - /// @dev A proxy as an implementation from before this one left it: open gate, version 2. + /// @dev A proxy as the previous implementation left it: open gate, version 3. function _legacyGateway() internal returns (IntentGatewayV2 gateway) { gateway = _freshInitializedGateway(); - vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(2))); - assertEq(gateway.version(), 2, "legacy proxy"); + vm.store(address(gateway), INITIALIZABLE_SLOT, bytes32(uint256(3))); + assertEq(gateway.version(), 3, "legacy proxy"); } function _openParams() internal view returns (Params memory) { @@ -4674,7 +4680,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { vm.prank(address(host)); intentGateway.onAccept(IncomingPostRequest({relayer: relayer, request: reopen})); assertEq(intentGateway.relayer(), address(0)); - assertEq(intentGateway.version(), 3, "reopening the gate is not a migration either"); + assertEq(intentGateway.version(), 4, "reopening the gate is not a migration either"); // With no relayer set the gate is open, so a delivery from anyone lands. uint256 before = usdc.balanceOf(filler); @@ -4758,7 +4764,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { PostRequest memory arm = _rotateRequest(relayer); vm.prank(address(host)); gateway.onAccept(IncomingPostRequest({relayer: filler, request: arm})); - assertEq(gateway.version(), 3, "a rotation leaves the version alone"); + assertEq(gateway.version(), 4, "a rotation leaves the version alone"); PostRequest memory another = _newDeploymentRequest(bytes("OTHER_CHAIN"), address(0xCAFE)); another.from = abi.encodePacked(address(gateway)); another.to = abi.encodePacked(address(gateway)); @@ -4803,7 +4809,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { vm.prank(address(host)); intentGateway.onAccept(IncomingPostRequest({relayer: relayer, request: rotate})); assertEq(intentGateway.relayer(), next, "rotated"); - assertEq(intentGateway.version(), 3, "neither is a migration"); + assertEq(intentGateway.version(), 4, "neither is a migration"); assertEq(intentGateway._nonce(), 2, "_nonce preserved"); assertEq(intentGateway._filled(filledCommitment), filler, "_filled preserved"); assertEq(intentGateway._orders(escrowedCommitment, 0), escrowedAmount, "_orders preserved"); @@ -4826,7 +4832,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { assertEq(_implementationOf(address(intentGateway)), address(newImpl)); assertEq(intentGateway.relayer(), relayer, "relayer survives an implementation swap"); - assertEq(intentGateway.version(), 3, "no migration ran, so the version is unchanged"); + assertEq(intentGateway.version(), 4, "no migration ran, so the version is unchanged"); } /// @dev Through the real host: a delivery the gateway refuses is recorded as undelivered, so @@ -4862,17 +4868,17 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { } } - /// @dev The proxy that is actually live on mainnet, on the fork: armed and migrated, closed to + /// @dev The proxy that is actually live on mainnet, on the fork: armed, closed to /// re-initialisation, and governed only by its own relayer, including the next upgrade, which - /// must keep every readable piece of state, and a rotation. + /// lands at `VERSION` with every other readable piece of state kept, and a rotation. function testLiveProxyIsArmedAndGovernedOnlyByItsRelayer() public { IntentGatewayV2 live = IntentGatewayV2(payable(LIVE_GATEWAY)); assertGt(LIVE_GATEWAY.code.length, 0, "live gateway present on the fork"); address liveRelayer = live.relayer(); assertTrue(liveRelayer != address(0), "live proxy is armed"); - uint64 liveVersion = live.version(); - // Without init data the upgrade below cannot migrate, so the proxy must need no migration. - assertGe(liveVersion, intentGateway.version(), "live proxy needs no migration"); + // What the deploy script prints for governance: `migrate()` while the live proxy is a version + // behind, nothing once it has caught up. + bytes memory migration = intentGatewayUpgradeInitialization(live); assertEq(vm.load(LIVE_GATEWAY, bytes32(uint256(13))), _relayerSlot(liveRelayer), "relayer alone in slot 13"); address implBefore = _implementationOf(LIVE_GATEWAY); @@ -4892,13 +4898,10 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { fees[i] = live._destinationProtocolFees(keccak256(peers[i])); } - // Nobody can initialise it again, and the host cannot migrate it again. + // Nobody can initialise it again. InitParams memory init = InitParams({params: p, peerChains: peers, relayer: filler, owner: address(this)}); vm.expectRevert(Initializable.InvalidInitialization.selector); live.initialize(init); - vm.prank(liveHost); - vm.expectRevert(Initializable.InvalidInitialization.selector); - live.migrate(address(this)); // The legacy `initialize` is not an entry point of the live implementation: the call // reverts with no data, as any unknown selector does. (bool legacyInitialized, bytes memory legacyReturn) = @@ -4917,7 +4920,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { to: abi.encodePacked(LIVE_GATEWAY), body: bytes.concat( bytes1(uint8(IntentsBase.RequestKind.Execute)), - abi.encodeCall(ExtrinsicIntents.upgradeToAndCall, (address(newImpl), "")) + abi.encodeCall(ExtrinsicIntents.upgradeToAndCall, (address(newImpl), migration)) ), timeoutTimestamp: 0 }); @@ -4933,13 +4936,13 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { assertTrue(implBefore != address(newImpl), "implementation actually changed"); assertEq(live.relayer(), liveRelayer, "relayer survives the upgrade"); assertEq(vm.load(LIVE_GATEWAY, bytes32(uint256(13))), _relayerSlot(liveRelayer), "slot 13 preserved"); - assertEq(live.version(), liveVersion, "version preserved"); + assertEq(live.version(), intentGateway.version(), "at VERSION after the upgrade"); assertEq(live.owner(), liveOwner, "owner preserved"); assertEq(live.pendingOwner(), livePendingOwner, "pending owner preserved"); assertEq(live.paused(), livePaused, "pause state preserved"); vm.prank(liveHost); vm.expectRevert(Initializable.InvalidInitialization.selector); - live.migrate(address(this)); + live.migrate(); vm.expectRevert(Initializable.InvalidInitialization.selector); live.initialize(init); assertEq(live._nonce(), nonce, "_nonce preserved"); @@ -4967,7 +4970,7 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { vm.prank(liveHost); live.onAccept(IncomingPostRequest({relayer: liveRelayer, request: rotate})); assertEq(live.relayer(), next, "rotated through Execute"); - assertEq(live.version(), liveVersion, "a rotation leaves the version alone"); + assertEq(live.version(), intentGateway.version(), "a rotation leaves the version alone"); vm.prank(liveHost); vm.expectRevert(IntentsBase.Unauthorized.selector); live.onAccept(IncomingPostRequest({relayer: liveRelayer, request: rotate})); @@ -5744,24 +5747,6 @@ contract IntentGatewayV2Test is MainnetForkBaseTest { ); } - /// `migrate` sets the owner of a proxy coming from an earlier implementation. - function testMigrateSetsTheOwner() public { - IntentGatewayV2 gateway = _legacyGateway(); - address next = makeCleanAddr("migratedOwner"); - - vm.prank(address(host)); - vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0))); - gateway.migrate(address(0)); - - vm.expectEmit(true, true, true, true, address(gateway)); - emit OwnableUpgradeable.OwnershipTransferred(address(this), next); - vm.prank(address(host)); - gateway.migrate(next); - - assertEq(gateway.owner(), next, "owner from the migration"); - assertEq(gateway.version(), 3); - } - function testOwnershipTransferIsTwoStep() public { address next = makeCleanAddr("nextOwner"); diff --git a/evm/tests/foundry/Permit2CompromiseForkTest.t.sol b/evm/tests/foundry/Permit2CompromiseForkTest.t.sol index e30c7cf26..fe70ad783 100644 --- a/evm/tests/foundry/Permit2CompromiseForkTest.t.sol +++ b/evm/tests/foundry/Permit2CompromiseForkTest.t.sol @@ -3,21 +3,8 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ISignatureTransfer} from "../../src/utils/SimplexPaymaster.sol"; - -/// @dev Permit2 AllowanceTransfer surface — the signature-less pull path. -interface IAllowanceTransfer { - function allowance(address owner, address token, address spender) - external - view - returns (uint160 amount, uint48 expiration, uint48 nonce); - - function transferFrom(address from, address to, uint160 amount, address token) external; - - function DOMAIN_SEPARATOR() external view returns (bytes32); - - function nonceBitmap(address owner, uint256 word) external view returns (uint256); -} +import {ISignatureTransfer} from "@uniswap/permit2/src/interfaces/ISignatureTransfer.sol"; +import {IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol"; /// @notice Answers: with the filler holding a uint256-max token approval to Permit2, can an /// attacker who fully controls the paymaster drain the filler's balance? Runs against diff --git a/evm/tests/foundry/SimplexPaymasterGasGriefTest.t.sol b/evm/tests/foundry/SimplexPaymasterGasGriefTest.t.sol index 3d0f0899a..48d1a4190 100644 --- a/evm/tests/foundry/SimplexPaymasterGasGriefTest.t.sol +++ b/evm/tests/foundry/SimplexPaymasterGasGriefTest.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IncomingPostRequest} from "@hyperbridge/core/interfaces/IApp.sol"; @@ -11,24 +12,8 @@ import {IDispatcher} from "@hyperbridge/core/interfaces/IDispatcher.sol"; import {SimplexPaymaster, AggregatorV3Interface} from "../../src/utils/SimplexPaymaster.sol"; import {SolverAccount} from "../../src/apps/intentsv2/SolverAccount.sol"; import {SimplexPaymasterHarness} from "./SimplexPaymasterTest.t.sol"; - -interface IPermit2Domain { - function DOMAIN_SEPARATOR() external view returns (bytes32); -} - -interface IEntryPointGas { - error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); - - function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; - - function getUserOpHash(PackedUserOperation calldata userOp) external view returns (bytes32); - - function depositTo(address account) external payable; - - function balanceOf(address account) external view returns (uint256); - - function getNonce(address sender, uint192 key) external view returns (uint256); -} +import {ISignatureTransfer} from "@uniswap/permit2/src/interfaces/ISignatureTransfer.sol"; +import {toEntryPointOp, toEntryPointOps} from "./EntryPointOps.sol"; /// @notice Measures whether the gas the paymaster is charged by the EntryPoint stays /// covered by the tokens it charges the user, when the user inflates gas @@ -36,8 +21,8 @@ interface IEntryPointGas { /// portion of `callGasLimit + paymasterPostOpGasLimit`; the paymaster only /// caps the latter. Runs against the real EntryPoint v0.9 on a fork. contract SimplexPaymasterGasGriefTest is Test { - IEntryPointGas constant ENTRY_POINT = IEntryPointGas(0x433709009B8330FDa32311DF1C2AFA402eD8D009); - IPermit2Domain constant PERMIT2 = IPermit2Domain(0x000000000022D473030F116dDEE9F6B43aC78BA3); + IEntryPoint constant ENTRY_POINT = IEntryPoint(0x433709009B8330FDa32311DF1C2AFA402eD8D009); + ISignatureTransfer constant PERMIT2 = ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3); bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" @@ -221,16 +206,16 @@ contract SimplexPaymasterGasGriefTest is Test { vm.prank(outsider, outsider); vm.expectRevert( abi.encodeWithSelector( - IEntryPointGas.FailedOpWithRevert.selector, + IEntryPoint.FailedOpWithRevert.selector, uint256(0), "AA33 reverted", abi.encodeWithSelector(SimplexPaymaster.UnauthorizedBundler.selector, outsider) ) ); - ENTRY_POINT.handleOps(ops, payable(beneficiary)); + ENTRY_POINT.handleOps(toEntryPointOps(ops), payable(beneficiary)); vm.prank(bundler, bundler); - ENTRY_POINT.handleOps(ops, payable(beneficiary)); + ENTRY_POINT.handleOps(toEntryPointOps(ops), payable(beneficiary)); assertEq(ENTRY_POINT.getNonce(solver, 0), nonce + 1); } @@ -247,13 +232,13 @@ contract SimplexPaymasterGasGriefTest is Test { vm.prank(bundler, bundler); vm.expectRevert( abi.encodeWithSelector( - IEntryPointGas.FailedOpWithRevert.selector, + IEntryPoint.FailedOpWithRevert.selector, uint256(0), "AA33 reverted", abi.encodeWithSelector(SimplexPaymaster.InvalidPaymasterData.selector, dataLength) ) ); - ENTRY_POINT.handleOps(ops, payable(beneficiary)); + ENTRY_POINT.handleOps(toEntryPointOps(ops), payable(beneficiary)); } function _run(uint128 callGasLimit) internal returns (uint256 weiCharged, uint256 nativeSpent) { @@ -281,7 +266,7 @@ contract SimplexPaymasterGasGriefTest is Test { PackedUserOperation[] memory ops = new PackedUserOperation[](1); ops[0] = op; vm.prank(bundler, bundler); - ENTRY_POINT.handleOps(ops, payable(beneficiary)); + ENTRY_POINT.handleOps(toEntryPointOps(ops), payable(beneficiary)); uint256 tokensCharged = tokensBefore - IERC20(token).balanceOf(solver); nativeSpent = depositBefore - ENTRY_POINT.balanceOf(address(paymaster)); @@ -321,11 +306,11 @@ contract SimplexPaymasterGasGriefTest is Test { abi.encodePacked(uint8(2), token, uint256(1_000e6), op.nonce, deadline, permitSig) ); if (paymasterSignerKey != 0) { - bytes memory paymasterSig = _sign(paymasterSignerKey, ENTRY_POINT.getUserOpHash(op)); + bytes memory paymasterSig = _sign(paymasterSignerKey, ENTRY_POINT.getUserOpHash(toEntryPointOp(op))); op.paymasterAndData = abi.encodePacked(op.paymasterAndData, paymasterSig, uint16(paymasterSig.length), PAYMASTER_SIG_MAGIC); } - op.signature = _sign(solverKey, ENTRY_POINT.getUserOpHash(op)); + op.signature = _sign(solverKey, ENTRY_POINT.getUserOpHash(toEntryPointOp(op))); } /// @dev v ‖ r ‖ s, the layout mode 0x02 expects. diff --git a/evm/tests/foundry/SimplexPaymasterMigrationForkTest.t.sol b/evm/tests/foundry/SimplexPaymasterMigrationForkTest.t.sol index 7d874175b..8041d94e3 100644 --- a/evm/tests/foundry/SimplexPaymasterMigrationForkTest.t.sol +++ b/evm/tests/foundry/SimplexPaymasterMigrationForkTest.t.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; -import {PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {IStakeManager} from "@account-abstraction/contracts/interfaces/IStakeManager.sol"; +import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -11,22 +13,9 @@ import {HyperApp} from "@hyperbridge/core/apps/HyperApp.sol"; import {IncomingPostRequest} from "@hyperbridge/core/interfaces/IApp.sol"; import {IDispatcher} from "@hyperbridge/core/interfaces/IDispatcher.sol"; -import {SimplexPaymaster, AggregatorV3Interface, IStakeManager} from "../../src/utils/SimplexPaymaster.sol"; +import {SimplexPaymaster, AggregatorV3Interface} from "../../src/utils/SimplexPaymaster.sol"; import {SolverAccount} from "../../src/apps/intentsv2/SolverAccount.sol"; - -interface IEntryPointFork { - function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; - - function getUserOpHash(PackedUserOperation calldata userOp) external view returns (bytes32); - - function getNonce(address sender, uint192 key) external view returns (uint256); - - function getDepositInfo(address account) external view returns (IStakeManager.DepositInfo memory info); -} - -interface IPermit2Domain { - function DOMAIN_SEPARATOR() external view returns (bytes32); -} +import {ISignatureTransfer} from "@uniswap/permit2/src/interfaces/ISignatureTransfer.sol"; /// @notice Delivers the EntryPoint v0.9 upgrade to the LIVE paymaster proxies the way governance /// will: the host hands `onAccept` an `UpgradeContract` request from Hyperbridge, submitted @@ -35,8 +24,8 @@ interface IPermit2Domain { abstract contract SimplexPaymasterMigrationForkTest is Test { using SafeERC20 for IERC20; - IEntryPointFork constant ENTRY_POINT_V08 = IEntryPointFork(0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108); - IEntryPointFork constant ENTRY_POINT_V09 = IEntryPointFork(0x433709009B8330FDa32311DF1C2AFA402eD8D009); + IEntryPoint constant ENTRY_POINT_V08 = IEntryPoint(0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108); + IEntryPoint constant ENTRY_POINT_V09 = IEntryPoint(0x433709009B8330FDa32311DF1C2AFA402eD8D009); address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address constant INTENT_GATEWAY = 0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716; bytes32 constant PERMIT_TYPEHASH = @@ -240,7 +229,7 @@ abstract contract SimplexPaymasterMigrationForkTest is Test { ) ); (uint8 v, bytes32 r, bytes32 s) = - vm.sign(solverKey, _digest(IPermit2Domain(PERMIT2).DOMAIN_SEPARATOR(), structHash)); + vm.sign(solverKey, _digest(ISignatureTransfer(PERMIT2).DOMAIN_SEPARATOR(), structHash)); _sponsor(solver, solverKey, token, abi.encodePacked(uint8(2), token, permitAmount, nonce, deadline, v, r, s)); } diff --git a/evm/tests/foundry/SimplexPaymasterPermit2ForkTest.t.sol b/evm/tests/foundry/SimplexPaymasterPermit2ForkTest.t.sol index 6075d1348..655e80160 100644 --- a/evm/tests/foundry/SimplexPaymasterPermit2ForkTest.t.sol +++ b/evm/tests/foundry/SimplexPaymasterPermit2ForkTest.t.sol @@ -5,26 +5,13 @@ import {Test} from "forge-std/Test.sol"; import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; -import {SimplexPaymaster, AggregatorV3Interface, ISignatureTransfer} from "../../src/utils/SimplexPaymaster.sol"; +import {SimplexPaymaster, AggregatorV3Interface} from "../../src/utils/SimplexPaymaster.sol"; +import {ISignatureTransfer} from "@uniswap/permit2/src/interfaces/ISignatureTransfer.sol"; import {SolverAccount} from "../../src/apps/intentsv2/SolverAccount.sol"; import {SimplexPaymasterHarness} from "./SimplexPaymasterTest.t.sol"; - -interface IPermit2Test { - function DOMAIN_SEPARATOR() external view returns (bytes32); - - function nonceBitmap(address owner, uint256 word) external view returns (uint256); -} - -interface IEntryPointTest { - function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; - - function getUserOpHash(PackedUserOperation calldata userOp) external view returns (bytes32); - - function depositTo(address account) external payable; - - function balanceOf(address account) external view returns (uint256); -} +import {toEntryPointOp, toEntryPointOps} from "./EntryPointOps.sol"; /// @notice Exercises PERMIT2 mode against the real Permit2, real stablecoins, /// real Chainlink feeds and the real EntryPoint v0.9 on a mainnet fork. @@ -42,7 +29,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { bytes4 constant INVALID_CONTRACT_SIGNATURE = 0xb0669cbc; // InvalidContractSignature() ISignatureTransfer constant PERMIT2 = ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3); - IEntryPointTest constant ENTRY_POINT = IEntryPointTest(0x433709009B8330FDa32311DF1C2AFA402eD8D009); + IEntryPoint constant ENTRY_POINT = IEntryPoint(0x433709009B8330FDa32311DF1C2AFA402eD8D009); address constant INTENT_GATEWAY = 0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716; // Per-chain fixtures supplied by the concrete test. @@ -125,7 +112,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { uint256 pulled = solverBefore - IERC20(stable).balanceOf(solver); assertGt(pulled, 0); assertEq(IERC20(stable).balanceOf(address(paymaster)), pulled); - assertEq(IPermit2Test(address(PERMIT2)).nonceBitmap(solver, 0) & (1 << nonce), 1 << nonce); + assertEq(PERMIT2.nonceBitmap(solver, 0) & (1 << nonce), 1 << nonce); assertEq(validationData, ERC4337Utils.packValidationData(true, 0, uint48(deadline))); // context = userOpHash(32) || token(20) || tokenPrice(32) || prefundAmount(32) || prefunder(20) assertEq(context.length, 0x88); @@ -198,7 +185,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { abi.encodeWithSelector(SimplexPaymaster.InsufficientPermitAmount.selector, required - 1, required) ); paymaster.validate(low, maxCost); - assertEq(IPermit2Test(address(PERMIT2)).nonceBitmap(solver, 0) & (1 << 7), 0); + assertEq(PERMIT2.nonceBitmap(solver, 0) & (1 << 7), 0); PackedUserOperation memory exact = _permit2Op(required, 7, deadline, solverKey); paymaster.validate(exact, maxCost); @@ -254,7 +241,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { ENTRY_POINT.depositTo{value: 1 ether}(address(paymaster)); PackedUserOperation memory op = _permit2Op(100 * stableUnit, 10, block.timestamp + 1 hours, solverKey); - op.signature = _sign(solverKey, ENTRY_POINT.getUserOpHash(op)); + op.signature = _sign(solverKey, ENTRY_POINT.getUserOpHash(toEntryPointOp(op))); uint256 solverBefore = IERC20(stable).balanceOf(solver); uint256 depositBefore = ENTRY_POINT.balanceOf(address(paymaster)); @@ -262,7 +249,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { PackedUserOperation[] memory ops = new PackedUserOperation[](1); ops[0] = op; vm.prank(bundler, bundler); - ENTRY_POINT.handleOps(ops, payable(beneficiary)); + ENTRY_POINT.handleOps(toEntryPointOps(ops), payable(beneficiary)); uint256 charged = solverBefore - IERC20(stable).balanceOf(solver); uint256 nativeSpent = depositBefore - ENTRY_POINT.balanceOf(address(paymaster)); @@ -274,7 +261,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { (,, uint256 tokenPrice) = paymaster.fetchDetails(op); uint256 prefund = ((_maxCost(op) + 30_000 * _maxFeePerGas(op)) * tokenPrice) / 1e18; assertLt(charged, prefund); - assertEq(IPermit2Test(address(PERMIT2)).nonceBitmap(solver, 0) & (1 << 10), 1 << 10); + assertEq(PERMIT2.nonceBitmap(solver, 0) & (1 << 10), 1 << 10); } // ── Helpers ────────────────────────────────────────────────────── @@ -326,7 +313,7 @@ abstract contract SimplexPaymasterPermit2ForkTest is Test { ) ); bytes32 digest = - keccak256(abi.encodePacked("\x19\x01", IPermit2Test(address(PERMIT2)).DOMAIN_SEPARATOR(), structHash)); + keccak256(abi.encodePacked("\x19\x01", PERMIT2.DOMAIN_SEPARATOR(), structHash)); // mode 0x02 lays the signature out as v ‖ r ‖ s (mirroring the EIP-2612 mode 0x00 fields). (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, digest); return abi.encodePacked(v, r, s); diff --git a/evm/tests/foundry/SimplexPaymasterTest.t.sol b/evm/tests/foundry/SimplexPaymasterTest.t.sol index d8dcf4808..85686239c 100644 --- a/evm/tests/foundry/SimplexPaymasterTest.t.sol +++ b/evm/tests/foundry/SimplexPaymasterTest.t.sol @@ -6,6 +6,7 @@ import {Vm} from "forge-std/Vm.sol"; import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {IPaymaster} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {PaymasterCore} from "@openzeppelin/community-contracts/contracts/account/paymaster/PaymasterCore.sol"; +import {IStakeManager} from "@account-abstraction/contracts/interfaces/IStakeManager.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; @@ -15,7 +16,7 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s import {HyperApp} from "@hyperbridge/core/apps/HyperApp.sol"; import {IncomingPostRequest} from "@hyperbridge/core/interfaces/IApp.sol"; -import {SimplexPaymaster, AggregatorV3Interface, IStakeManager} from "../../src/utils/SimplexPaymaster.sol"; +import {SimplexPaymaster, AggregatorV3Interface} from "../../src/utils/SimplexPaymaster.sol"; contract MockHost { bytes public hyperbridgeId; diff --git a/evm/tests/foundry/account/SolverAccountTest.sol b/evm/tests/foundry/account/SolverAccountTest.sol index ece7e06ba..0f730ccfa 100644 --- a/evm/tests/foundry/account/SolverAccountTest.sol +++ b/evm/tests/foundry/account/SolverAccountTest.sol @@ -4,10 +4,11 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import {SolverAccount} from "../../../src/apps/intentsv2/SolverAccount.sol"; import {IntentGatewayV2} from "../../../src/apps/IntentGatewayV2.sol"; -import {IntentsBase, IEntryPointV09} from "../../../src/apps/intentsv2/IntentsBase.sol"; +import {IntentsBase} from "../../../src/apps/intentsv2/IntentsBase.sol"; import {deployIntentGatewayImpl, deployIntentModules} from "../IntentGatewayDeploy.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IntentQuoteTestUtils} from "../IntentQuoteTestUtils.sol"; +import {toEntryPointOp, toEntryPointOps} from "../EntryPointOps.sol"; import {ERC20Token} from "../mocks/ERC20Token.sol"; import { SelectOptions, @@ -26,6 +27,7 @@ import {Account as AccountBase} from "@openzeppelin/contracts/account/Account.so import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {Execution} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; @@ -111,23 +113,11 @@ contract SolverAccountTest is Test { // ============================================ function test_ReleaseVersionProtectsCurrentAndHistoricalSelectors() public view { - assertEq(intentGateway.version(), 3); + assertEq(intentGateway.version(), 4); assertNotEq(intentGateway.fillOrder.selector, bytes4(0xa5470064)); assertNotEq(intentGateway.fillOrder.selector, bytes4(0x5cfb1ea5)); } - function testVersionTwoMigrationShiftsRelayerOnce() public { - bytes32 initSlot = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - vm.store(address(intentGateway), initSlot, bytes32(uint256(2))); - address relayer = address(0x123456); - vm.store(address(intentGateway), bytes32(uint256(13)), bytes32(uint256(uint160(relayer)) << 8)); - vm.prank(intentGateway.host()); - intentGateway.migrate(address(0xabc)); - assertEq(intentGateway.version(), 3); - assertEq(intentGateway.owner(), address(0xabc)); - assertEq(intentGateway.relayer(), relayer); - } - function test_Constructor_SetsCachedValues() public view { assertEq(address(solverAccount.entryPoint()), entryPoint); @@ -1195,7 +1185,7 @@ contract SolverAccountTest is Test { (ops[1],) = _handleOpsBid(commitment, callData, sessionKeyPrivateKey); vm.expectEmit(true, true, false, true, entryPoint); - emit IEntryPointHandleOps.UserOperationRevertReason( + emit IEntryPoint.UserOperationRevertReason( otherHash, address(solverAccount), ops[0].nonce, abi.encodeWithSelector(IntentsBase.Unauthorized.selector) ); _handleOps(ops); @@ -1388,7 +1378,7 @@ contract SolverAccountTest is Test { /// @dev Runs a bid's batch as the EntryPoint does while it reports `executing` as the current op. function _executeBid(bytes memory callData, bytes32 executing) internal { - bytes memory currentUserOpHashCall = abi.encodeCall(IEntryPointV09.getCurrentUserOpHash, ()); + bytes memory currentUserOpHashCall = abi.encodeCall(IEntryPoint.getCurrentUserOpHash, ()); vm.mockCall(entryPoint, currentUserOpHashCall, abi.encode(executing)); vm.prank(entryPoint); (bool ok, bytes memory returned) = address(solverAccount).call(callData); @@ -1412,7 +1402,7 @@ contract SolverAccountTest is Test { op.accountGasLimits = bytes32((uint256(300_000) << 128) | uint256(1_000_000)); op.preVerificationGas = 60_000; op.gasFees = bytes32((uint256(1 gwei) << 128) | (block.basefee + 1 gwei)); - userOpHash = IEntryPointHandleOps(entryPoint).getUserOpHash(op); + userOpHash = IEntryPoint(entryPoint).getUserOpHash(toEntryPointOp(op)); op.signature = abi.encodePacked( commitment, _signUserOpHash(userOpHash), _signSelection(commitment, userOpHash, selectorPrivateKey) ); @@ -1422,7 +1412,7 @@ contract SolverAccountTest is Test { function _handleOps(PackedUserOperation[] memory ops) internal { address bundler = makeAddr("bundler"); vm.prank(bundler, bundler); - IEntryPointHandleOps(entryPoint).handleOps(ops, payable(bundler)); + IEntryPoint(entryPoint).handleOps(toEntryPointOps(ops), payable(bundler)); } /// @notice ERC-7821 execute(mode, executionData) calldata for a batch of calls @@ -1532,17 +1522,6 @@ contract SolverAccountTest is Test { } } -/// @dev The EntryPoint v0.9 calls a bundler makes, and the event an op that reverts in execution emits. -interface IEntryPointHandleOps { - event UserOperationRevertReason( - bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason - ); - - function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; - - function getUserOpHash(PackedUserOperation calldata userOp) external view returns (bytes32); -} - contract MockContract { fallback() external payable {} } diff --git a/sdk/packages/core/contracts/apps/IntentGatewayV2.sol b/sdk/packages/core/contracts/apps/IntentGatewayV2.sol index bcb65d14e..4bdf47414 100644 --- a/sdk/packages/core/contracts/apps/IntentGatewayV2.sol +++ b/sdk/packages/core/contracts/apps/IntentGatewayV2.sol @@ -284,7 +284,7 @@ interface IIntentGatewayV2 { /// @notice Thrown when an owner-only function is called by anyone but the owner or the host. error OwnableUnauthorizedAccount(address account); - /// @notice Thrown when `initialize` or `migrate` is given a zero owner. + /// @notice Thrown when `initialize` is given a zero owner. error OwnableInvalidOwner(address owner); // ============================================ @@ -426,7 +426,7 @@ interface IIntentGatewayV2 { event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); /** - * @notice Emitted when the owner is set, by `initialize`, `migrate`, `acceptOwnership` or + * @notice Emitted when the owner is set, by `initialize`, `acceptOwnership` or * `renounceOwnership`. * @param previousOwner The owner before this change * @param newOwner The owner from now on @@ -505,15 +505,6 @@ interface IIntentGatewayV2 { */ function relayer() external view returns (address); - /** - * @notice Takes a proxy from an earlier implementation to the current version, where - * `initialize` puts a fresh one. Host-only and one-shot; emits `Initialized`. It is the - * only way up for a proxy already at a version: `initialize` is refused on anything but - * a bare proxy. Moves the relayer from slot 13 offset 1 to offset 0 and sets the owner. - * @param owner The owner, who may pause the gateway; must be non-zero - */ - function migrate(address owner) external; - /** * @notice The owner, who may pause and resume the gateway. * @return address The owner @@ -555,9 +546,10 @@ interface IIntentGatewayV2 { function unpause() external; /** - * @notice The `Initializable` version: 3 once `initialize` or `migrate` has run on the - * module-split implementation with an owner, 2 on the armed implementation before it, 1 - * before the relayer gate. Reverts on implementations that predate the gate. + * @notice The `Initializable` version: 4 once `initialize` or `migrate` has run on the + * implementation that binds solver selection to the EntryPoint v0.9 UserOperation, 3 on + * the module-split implementation with an owner, 2 on the armed implementation before + * it, 1 before the relayer gate. Reverts on implementations that predate the gate. * @return uint64 The initialized version */ function version() external view returns (uint64); diff --git a/sdk/packages/core/docs/ai/flows/how-a-cross-chain-delivery-reaches-the-gateway-and-where-the.md b/sdk/packages/core/docs/ai/flows/how-a-cross-chain-delivery-reaches-the-gateway-and-where-the.md index 721fb088b..a5c51f40f 100644 --- a/sdk/packages/core/docs/ai/flows/how-a-cross-chain-delivery-reaches-the-gateway-and-where-the.md +++ b/sdk/packages/core/docs/ai/flows/how-a-cross-chain-delivery-reaches-the-gateway-and-where-the.md @@ -61,8 +61,8 @@ action, `Execute` (discriminator 5): `onAccept` delegatecalls the module's own a with `body[1:]` as calldata, another hop that keeps the host as `msg.sender` so `onlyHost` passes. `setRelayer(next)` as that calldata is a rotation, `upgradeToAndCall(newImpl, initData)` is an upgrade, and inside the latter `ERC1967Utils.upgradeToAndCall` delegatecalls `initData` into the -new implementation, still with the host as `msg.sender`, which is how `migrate(owner)` sets the -owner and bumps the version in the same transaction as the swap. `setRelayer` and `upgradeToAndCall` exist only on the +new implementation, still with the host as `msg.sender`, which is how `migrate()` bumps the +version in the same transaction as the swap. `setRelayer` and `upgradeToAndCall` exist only on the module, not on the implementation, so init data cannot rotate the relayer: an upgrade and a rotation are two `Execute` messages (`testUpgradeThenRotateAreTwoExecutes`). A revert anywhere inside bubbles out of `onAccept`, so the host records the message undelivered. The pallet's @@ -89,13 +89,16 @@ accepts the host, so governance can pause, resume or propose an owner with an `E `upgradeToAndCall(currentImplementation, call)` (`testGovernanceReplacesTheOwnerThroughExecute`, `testHostCountsAsOwner`). A fresh proxy is armed by its init data: `initialize` takes an `InitParams` struct (`params`, `peerChains`, `relayer`, `owner`), writes the relayer and owner through `_setRelayer` and `__Ownable_init`, -and lands at `VERSION` (3) under `reinitializer`, emitting `RelayerUpdated`, `OwnershipTransferred` -then `Initialized(3)`; it is refused on any proxy already at a version. A proxy on the previous -implementation sits at 2 until the upgrade whose init data is `abi.encodeCall(migrate, (owner))`, -host-only and under the same `reinitializer(VERSION)`, moves `_relayer` from slot 13 offset 1 to offset 0, sets the owner and takes it to 3; that is the only way up for it, since `initialize` is refused on -anything but a bare proxy. A `setRelayer` rotation leaves the version alone. A revert from -`version()` means an implementation from before the gate. `testInitializeArmsTheGate` pins the -fresh path, `testMigrateMovesTheRelayerToOffsetZero`, `testMigrateSetsTheOwner` and `testMigrateRunsOnce` the -migration, `testUpgradeFromVersionTwoWithMigrate` (`evm/tests/foundry/IntentGatewayModulesTest.sol`) -the release's own upgrade from 2, and the live-fork test reads 2 on the mainnet proxy, upgrades it -with `migrate(owner)` to 3, checks its relayer now reads from offset 0, and shows it refuses `initialize` and a second `migrate`. +and lands at `VERSION` (4) under `reinitializer`, emitting `RelayerUpdated`, `OwnershipTransferred` +then `Initialized(4)`; it is refused on any proxy already at a version. A proxy on the previous +implementation sits at 3 until the upgrade whose init data is `abi.encodeCall(migrate, ())`, +host-only and under the same `reinitializer(VERSION)`, takes it to 4. Storage is the same at 3 and 4, +so `migrate` writes nothing else. It runs only on a proxy exactly one version behind, so an older +proxy cannot skip a migration. It is the only way up, since `initialize` is refused on anything but +a bare proxy. A `setRelayer` rotation leaves the version alone. A revert from `version()` means an +implementation from before the gate. `testInitializeArmsTheGate` pins the fresh path, +`testMigrateOnlyBumpsTheVersion`, `testMigrateRefusesOlderVersions` and `testMigrateRunsOnce` the +migration, `testUpgradeFromVersionThreeWithMigrate` (`evm/tests/foundry/IntentGatewayModulesTest.sol`) +the release's own upgrade from 3, and the live-fork test upgrades the mainnet proxy with the init +data `intentGatewayUpgradeInitialization` builds, checks it lands at `VERSION` with its relayer and +owner intact, and shows it refuses `initialize` and a second `migrate`. diff --git a/sdk/packages/indexer/scripts/tests/solver-fixtures.cjs b/sdk/packages/indexer/scripts/tests/solver-fixtures.cjs index f11c06828..2b8a1c16c 100644 --- a/sdk/packages/indexer/scripts/tests/solver-fixtures.cjs +++ b/sdk/packages/indexer/scripts/tests/solver-fixtures.cjs @@ -19,7 +19,7 @@ const USDC = { const VAULT = "0xc768c589647798a6ee01a91fde98ef2ed046dbd6" /** A SolverAccount from the Base config: delegation counts only when it points at one of these. */ -const SOLVER_ACCOUNT = "0x77c3394ca5881a74f18139ac87d0c11f8faa90cc" +const SOLVER_ACCOUNT = "0xaad062555800a97af062795189e32a3cbd045612" /** Not a SolverAccount, so a 7702 designator pointing here is recorded but not counted. */ const FOREIGN_DELEGATE = "0x00000000000000000000000000000000000de1e6" diff --git a/sdk/packages/indexer/src/configs/config-mainnet.json b/sdk/packages/indexer/src/configs/config-mainnet.json index adbf488b6..6223e1841 100644 --- a/sdk/packages/indexer/src/configs/config-mainnet.json +++ b/sdk/packages/indexer/src/configs/config-mainnet.json @@ -47,7 +47,7 @@ "handlerV2": "0x2a18AB35DEa43474882E05A661e2F20fe89c0535", "intentGatewayV3": "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", "solverAccount": [ - "0xd5535d4DeB17F050e52B6efda2fDe00435f39279", + "0xaAd062555800a97Af062795189e32a3CBd045612", "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" ], "yieldVaults": { @@ -117,7 +117,7 @@ "handlerV2": "0x2a18AB35DEa43474882E05A661e2F20fe89c0535", "intentGatewayV3": "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", "solverAccount": [ - "0xd5535d4DeB17F050e52B6efda2fDe00435f39279", + "0xaAd062555800a97Af062795189e32a3CBd045612", "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" ], "yieldVaults": { @@ -163,7 +163,7 @@ "handlerV2": "0x2a18AB35DEa43474882E05A661e2F20fe89c0535", "intentGatewayV3": "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", "solverAccount": [ - "0xd5535d4DeB17F050e52B6efda2fDe00435f39279", + "0xaAd062555800a97Af062795189e32a3CBd045612", "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" ], "yieldVaults": { @@ -231,7 +231,7 @@ "handlerV2": "0x2a18AB35DEa43474882E05A661e2F20fe89c0535", "intentGatewayV3": "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", "solverAccount": [ - "0xd5535d4DeB17F050e52B6efda2fDe00435f39279", + "0xaAd062555800a97Af062795189e32a3CBd045612", "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" ], "yieldVaults": { @@ -254,6 +254,10 @@ "0xa8aea66b361a8d53e8865c62d142167af28af058": { "description": "cNGN \u2014 listed so the LP balance sweep tracks the raw ERC-20 balance", "vaults": [] + }, + "0x5b0c50fdd52ecc0d4c682c441eabad41ffdeabbb": { + "description": "BRIDGE \u2014 listed so the LP balance sweep tracks the raw ERC-20 balance", + "vaults": [] } }, "tokenSlots": { @@ -299,7 +303,7 @@ "handlerV2": "0x2a18AB35DEa43474882E05A661e2F20fe89c0535", "intentGatewayV3": "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", "solverAccount": [ - "0xd5535d4DeB17F050e52B6efda2fDe00435f39279", + "0xaAd062555800a97Af062795189e32a3CBd045612", "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" ], "yieldVaults": { @@ -334,6 +338,10 @@ "0xdc3326e71d45186f113a2f448984ca0e8d201995": { "description": "XSGD \u2014 listed so the LP balance sweep tracks the raw ERC-20 balance", "vaults": [] + }, + "0x5b0c50fdd52ecc0d4c682c441eabad41ffdeabbb": { + "description": "BRIDGE \u2014 listed so the LP balance sweep tracks the raw ERC-20 balance", + "vaults": [] } }, "tokenSlots": { diff --git a/sdk/packages/indexer/src/configs/config-solver-ci.json b/sdk/packages/indexer/src/configs/config-solver-ci.json index e7310a60a..9e165c59a 100644 --- a/sdk/packages/indexer/src/configs/config-solver-ci.json +++ b/sdk/packages/indexer/src/configs/config-solver-ci.json @@ -17,7 +17,7 @@ "erc6160ext20": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", "handlerV2": "0x2a18AB35DEa43474882E05A661e2F20fe89c0535", "intentGatewayV3": "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - "solverAccount": ["0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC"], + "solverAccount": ["0xaAd062555800a97Af062795189e32a3CBd045612"], "yieldVaults": { "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { "description": "USDC → stataUSDC (Aave v3 Base) — the one supported token the E2E seeds and asserts on", diff --git a/sdk/packages/indexer/src/services/__tests__/solverInventory.service.test.ts b/sdk/packages/indexer/src/services/__tests__/solverInventory.service.test.ts index a551b90d8..ea68a4d2c 100644 --- a/sdk/packages/indexer/src/services/__tests__/solverInventory.service.test.ts +++ b/sdk/packages/indexer/src/services/__tests__/solverInventory.service.test.ts @@ -40,7 +40,7 @@ jest.mock("@/yield-vault-addresses", () => ({ }, })) jest.mock("@/solver-account-addresses", () => ({ - SOLVER_ACCOUNT_ADDRESSES: { "EVM-8453": ["0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC"] }, + SOLVER_ACCOUNT_ADDRESSES: { "EVM-8453": ["0xaAd062555800a97Af062795189e32a3CBd045612"] }, })) import { ethers } from "ethers" @@ -68,7 +68,7 @@ const VAULT = "0xc768c589647798a6ee01a91fde98ef2ed046dbd6" const SOLVER = "0xce319986ca4d5d0893751a628d0db3dc8fc91d62" const OTHER = "0x13e41cde1d55880cbe031c69f206c2e9bc3c94c2" const ZERO = "0x0000000000000000000000000000000000000000" -const SOLVER_ACCOUNT = "0x77c3394ca5881a74f18139ac87d0c11f8faa90cc" +const SOLVER_ACCOUNT = "0xaad062555800a97af062795189e32a3cbd045612" const DELEGATED = `0xef0100${SOLVER_ACCOUNT.slice(2)}` const T0 = 1_750_000_000n diff --git a/sdk/packages/sdk/docs/ai/decisions/2026-09-18-the-gateway-must-report-release-3.md b/sdk/packages/sdk/docs/ai/decisions/2026-09-18-the-gateway-must-report-release-3.md deleted file mode 100644 index 2d4ab52d6..000000000 --- a/sdk/packages/sdk/docs/ai/decisions/2026-09-18-the-gateway-must-report-release-3.md +++ /dev/null @@ -1,18 +0,0 @@ -# 2026-09-18 — The gateway must report release 3 - -The SDK speaks one `fillOrder` shape, the one with a take per leg in `FillOptions.inputs` -(selector `0x68ddf058`). `assertGatewayRelease` reads `version()` on the gateway and throws unless -it reports `3`; a gateway without the getter, or on any other release, is refused rather than -encoded for. `supportsRateFills` and `readRateFillCapability` apply the same check, and a bid is -signed or counted only when the destination gateway reports release 3. - -`SolverAccount` has no `version()` getter, so the account is not version-checked. Phantom bids still -require the sender to be delegated to one of the chain's configured `SolverAccount` addresses. - -Nothing is cached. A proxy keeps its address across upgrades, so a cached answer could outlive the -deployment it described. A revert or empty return is "no getter"; a transport or provider error -propagates. - -Earlier SDK releases carried the pre-quote shapes and resolved them from the ERC-1967 -implementation address. They stay published for gateways that have not been upgraded; this SDK -release does not talk to them. diff --git a/sdk/packages/sdk/docs/ai/decisions/2026-09-18-the-gateway-must-report-the-supported-release.md b/sdk/packages/sdk/docs/ai/decisions/2026-09-18-the-gateway-must-report-the-supported-release.md new file mode 100644 index 000000000..4151dbbd3 --- /dev/null +++ b/sdk/packages/sdk/docs/ai/decisions/2026-09-18-the-gateway-must-report-the-supported-release.md @@ -0,0 +1,24 @@ +# 2026-09-18 — The gateway must report the supported release + +The SDK speaks one gateway release, `SUPPORTED_INTENTS_VERSION`. It is 4: the release that binds +solver selection to the EntryPoint v0.9 UserOperation. Release 4 keeps the `fillOrder` shape release +3 introduced, with a take per leg in `FillOptions.inputs` (selector `0x68ddf058`). +`assertGatewayRelease` reads `version()` on the gateway and throws unless it reports that release. +A gateway without the getter, or on any other release, is refused rather than encoded for. +`supportsRateFills` applies the same check, and a bid is signed or counted only when the destination +gateway reports the supported release. + +Testnet chains (`TESTNET_CHAINS`) also accept release 3. The testnet gateways already run the +userOpHash solver selection that release 4 brings to mainnet, but still report 3. Both checks take the +destination's state machine id to tell the two apart. Mainnet accepts 4 only. + +`SolverAccount` has no `version()` getter, so the account is not version-checked. Phantom bids still +require the sender to be delegated to one of the chain's configured `SolverAccount` addresses. + +Nothing is cached. A proxy keeps its address across upgrades, so a cached answer could outlive the +deployment it described. A revert or empty return is "no getter"; a transport or provider error +propagates. + +Earlier SDK releases stay published for gateways that have not been upgraded. An SDK pinned to +release 3 keeps working against a gateway until governance upgrades it to 4, then refuses it, so a +filler has to move to this SDK release when its gateways move. diff --git a/sdk/packages/sdk/package.json b/sdk/packages/sdk/package.json index 5fb270762..348be2b39 100644 --- a/sdk/packages/sdk/package.json +++ b/sdk/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@hyperbridge/sdk", - "version": "2.9.0", + "version": "3.0.0", "description": "The hyperclient SDK provides utilities for querying proofs and statuses for cross-chain requests from HyperBridge.", "type": "module", "types": "./dist/node/index.d.ts", diff --git a/sdk/packages/sdk/src/configs/chain.ts b/sdk/packages/sdk/src/configs/chain.ts index 99b6f78c6..32eb34dd1 100644 --- a/sdk/packages/sdk/src/configs/chain.ts +++ b/sdk/packages/sdk/src/configs/chain.ts @@ -149,6 +149,7 @@ export type ConfiguredAssetSymbol = | "XSGD" | "TRYB" | "USDR" + | "BRIDGE" /** A configured asset symbol in its canonical, lowercase, or uppercase form. */ export type ConfiguredAssetSymbolInput = @@ -186,6 +187,8 @@ export interface ChainConfigData { XSGD?: string TRYB?: string USDR?: string + /** BridgeToken, the EVM representation of the nexus-native BRIDGE. */ + BRIDGE?: string } tokenDecimals?: { USDC: number @@ -197,6 +200,7 @@ export interface ChainConfigData { XSGD?: number TRYB?: number USDR?: number + BRIDGE?: number } tokenStorageSlots?: { USDT?: { balanceSlot: number; allowanceSlot: number } @@ -209,6 +213,7 @@ export interface ChainConfigData { XSGD?: { balanceSlot: number; allowanceSlot: number } TRYB?: { balanceSlot: number; allowanceSlot: number } USDR?: { balanceSlot: number; allowanceSlot: number } + BRIDGE?: { balanceSlot: number; allowanceSlot: number } } addresses: { IntentGateway?: `0x${string}` @@ -412,7 +417,7 @@ export const chainConfigs: Record = { }, addresses: { IntentGateway: "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - SolverAccount: "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC", + SolverAccount: "0xaAd062555800a97Af062795189e32a3CBd045612", TokenGateway: "0xFd413e3AFe560182C4471F4d143A96d3e259B6dE", Host: "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3", UniswapRouter02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", @@ -426,7 +431,7 @@ export const chainConfigs: Record = { UniswapV4StateView: "0x7ffe42c4a5deea5b0fec41c94c136cf115597227", Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333", Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", - EntryPoint: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + EntryPoint: "0x433709009B8330FDa32311DF1C2AFA402eD8D009", CirclePaymaster: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec", SimplexPaymaster: "0xD4340d7466e040626383cb9cda9307ba8E081149", Usdt0Oft: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee", @@ -459,6 +464,7 @@ export const chainConfigs: Record = { USDT: "0x55d398326f99059ff775485246999027b3197955", EXT: "0x7C8c11ADb8EF7cd3CFa718008Ea048445C6E7209", cNGN: "0xa8AEA66B361a8d53e8865c62D142167Af28Af058", + BRIDGE: "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB", }, tokenDecimals: { USDC: 18, @@ -468,6 +474,7 @@ export const chainConfigs: Record = { // by this, so the divergence from its neighbours here is load-bearing, not a typo. cNGN: 6, EXT: 18, + BRIDGE: 18, }, tokenStorageSlots: { USDT: { balanceSlot: 1, allowanceSlot: 2 }, @@ -475,10 +482,11 @@ export const chainConfigs: Record = { WETH: { balanceSlot: 3, allowanceSlot: 4 }, DAI: { balanceSlot: 0, allowanceSlot: 0 }, cNGN: { balanceSlot: 201, allowanceSlot: 202 }, // custom upgradeable layout, as on Base + BRIDGE: { balanceSlot: 0, allowanceSlot: 1 }, }, addresses: { IntentGateway: "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - SolverAccount: "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC", + SolverAccount: "0xaAd062555800a97Af062795189e32a3CBd045612", TokenGateway: "0xFd413e3AFe560182C4471F4d143A96d3e259B6dE", Host: "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3", UniswapRouter02: "0x10ED43C718714eb63d5aA57B78B54704E256024E", @@ -492,7 +500,7 @@ export const chainConfigs: Record = { UniswapV4StateView: "0xd13dd3d6e93f276fafc9db9e6bb47c1180aee0c4", Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333", Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", - EntryPoint: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + EntryPoint: "0x433709009B8330FDa32311DF1C2AFA402eD8D009", SimplexPaymaster: "0xeD02f9f0df8F562B89cC5b25867Ad3C2d61252A9", // "Usdt0Oft": Not available on BSC }, @@ -539,7 +547,7 @@ export const chainConfigs: Record = { }, addresses: { IntentGateway: "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - SolverAccount: "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC", + SolverAccount: "0xaAd062555800a97Af062795189e32a3CBd045612", TokenGateway: "0xFd413e3AFe560182C4471F4d143A96d3e259B6dE", Host: "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3", UniswapRouter02: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24", @@ -553,7 +561,7 @@ export const chainConfigs: Record = { UniswapV4StateView: "0x76fd297e2d437cd7f76d50f01afe6160f86e9990", Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333", Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", - EntryPoint: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + EntryPoint: "0x433709009B8330FDa32311DF1C2AFA402eD8D009", CirclePaymaster: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec", SimplexPaymaster: "0x7281Bccb4f0BCE44F3B8542d1fC5e51c2F5fC08C", Usdt0Oft: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92", @@ -611,7 +619,7 @@ export const chainConfigs: Record = { }, addresses: { IntentGateway: "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - SolverAccount: "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC", + SolverAccount: "0xaAd062555800a97Af062795189e32a3CBd045612", TokenGateway: "0xFd413e3AFe560182C4471F4d143A96d3e259B6dE", Host: "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3", UniswapRouter02: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24", @@ -622,7 +630,7 @@ export const chainConfigs: Record = { UniswapV4Quoter: "0x0d5e0f971ed27fbff6c2837bf31316121532048d", Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333", Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", - EntryPoint: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + EntryPoint: "0x433709009B8330FDa32311DF1C2AFA402eD8D009", CirclePaymaster: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec", SimplexPaymaster: "0x15b3B03C870c7ef252029c35A12d3b339F5c8d7f", AerodromeRouter: "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43", @@ -662,6 +670,7 @@ export const chainConfigs: Record = { ZARP: "0xb755506531786C8aC63B756BaB1ac387bACB0C04", XSGD: "0xDC3326e71D45186F113a2F448984CA0e8D201995", USDR: "0x3B5F2810fB2168FfA9C73160F97BF9f2461fFa5c", + BRIDGE: "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB", }, tokenDecimals: { USDC: 6, @@ -671,6 +680,7 @@ export const chainConfigs: Record = { ZARP: 18, XSGD: 6, USDR: 6, + BRIDGE: 18, }, tokenStorageSlots: { USDT: { balanceSlot: 0, allowanceSlot: 1 }, @@ -681,10 +691,11 @@ export const chainConfigs: Record = { ZARP: { balanceSlot: 51, allowanceSlot: 52 }, XSGD: { balanceSlot: 7, allowanceSlot: 8 }, USDR: { balanceSlot: 51, allowanceSlot: 52 }, + BRIDGE: { balanceSlot: 0, allowanceSlot: 1 }, }, addresses: { IntentGateway: "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - SolverAccount: "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC", + SolverAccount: "0xaAd062555800a97Af062795189e32a3CBd045612", TokenGateway: "0x8b536105b6Fae2aE9199f5146D3C57Dfe53b614E", Host: "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3", UniswapRouter02: "0xd2f9496824951D5237cC71245D659E48d0d5f9E8", @@ -698,7 +709,7 @@ export const chainConfigs: Record = { UniswapV4StateView: "0x5ea1bd7974c8a611cbab0bdcafcb1d9cc9b3ba5a", Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333", Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", - EntryPoint: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + EntryPoint: "0x433709009B8330FDa32311DF1C2AFA402eD8D009", CirclePaymaster: "0x0578cFB241215b77442a541325d6A4E6dFE700Ec", SimplexPaymaster: "0xe99acFe0f5fC4C8ea54A187D8D3b05f136150095", Usdt0Oft: "0x6BA10300f0DC58B7a1e4c0e41f5daBb7D7829e13", @@ -989,7 +1000,7 @@ export const chainConfigs: Record = { }, addresses: { IntentGateway: "0xAe041F7B0CB581876832830baeB6a2Aa2a3C9716", - SolverAccount: "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC", + SolverAccount: "0xaAd062555800a97Af062795189e32a3CBd045612", Host: "0x620128E2B19193d6Bd244a3AC8D3bBa0541B19c3", Calldispatcher: "0xE2C7e576E26E0bE7aC97c6fE925bcDAbD87c4bEd", }, diff --git a/sdk/packages/sdk/src/protocols/intents/BidManager.ts b/sdk/packages/sdk/src/protocols/intents/BidManager.ts index d66347318..feb4ca42a 100644 --- a/sdk/packages/sdk/src/protocols/intents/BidManager.ts +++ b/sdk/packages/sdk/src/protocols/intents/BidManager.ts @@ -91,7 +91,7 @@ export class BidManager { } const chain = normalizeStateMachineId(order.destination) const gateway = this.ctx.dest.configService.getIntentGatewayAddress(chain) - if (!(await supportsRateFills(this.ctx.dest.client as any, gateway))) { + if (!(await supportsRateFills(this.ctx.dest.client as any, gateway, chain))) { throw new Error("Fills are not supported by the destination gateway") } diff --git a/sdk/packages/sdk/src/protocols/intents/GasEstimator.ts b/sdk/packages/sdk/src/protocols/intents/GasEstimator.ts index eb36de16f..bf4c276ec 100644 --- a/sdk/packages/sdk/src/protocols/intents/GasEstimator.ts +++ b/sdk/packages/sdk/src/protocols/intents/GasEstimator.ts @@ -300,7 +300,7 @@ export class GasEstimator { let maxPriorityFeePerGas = gasPrice + (gasPrice * BigInt(priorityFeeBumpPercent)) / 100n let maxFeePerGas = gasPrice + (gasPrice * BigInt(maxFeeBumpPercent)) / 100n - await assertGatewayRelease(this.ctx.dest.client as any, intentGatewayV2Address) + await assertGatewayRelease(this.ctx.dest.client as any, intentGatewayV2Address, destStateMachineId) const fillOrderCalldata = encodeFillOrder(transformOrderForContract(order) as any, fillOptions) let callGasLimit: bigint = 500_000n diff --git a/sdk/packages/sdk/src/protocols/intents/fillOrderCodec.ts b/sdk/packages/sdk/src/protocols/intents/fillOrderCodec.ts index a0ba9ab4f..6c0f52e7c 100644 --- a/sdk/packages/sdk/src/protocols/intents/fillOrderCodec.ts +++ b/sdk/packages/sdk/src/protocols/intents/fillOrderCodec.ts @@ -1,6 +1,7 @@ import { encodeFunctionData, decodeFunctionData, type PublicClient } from "viem" import { ABI as IntentGatewayV2ABI } from "@/abis/IntentGatewayV2" import { isRevert } from "./escrowReads" +import { TESTNET_CHAINS } from "@/utils" import type { FillOptions, HexString, Order } from "@/types" export type DecodedFillOrder = { order: Order; options: FillOptions } @@ -8,7 +9,17 @@ export type DecodedFillOrder = { order: Order; options: FillOptions } /** `fillOrder(Order, FillOptions)` selector, pinned by codec tests; avoids import-time hashing in VM2. */ export const FILL_ORDER_SELECTOR = "0x68ddf058" as const /** The gateway release this SDK speaks. SolverAccount carries no version, so only the gateway is read. */ -export const SUPPORTED_INTENTS_VERSION = 3n +export const SUPPORTED_INTENTS_VERSION = 4n +/** + * Testnet gateways already run the userOpHash solver selection that release 4 brings to mainnet, but + * report 3, so a testnet chain accepts both. + */ +const TESTNET_INTENTS_VERSIONS = [3n, SUPPORTED_INTENTS_VERSION] + +/** The releases a gateway on `stateMachineId` may report. */ +function supportedReleases(stateMachineId: string): bigint[] { + return TESTNET_CHAINS.has(stateMachineId) ? TESTNET_INTENTS_VERSIONS : [SUPPORTED_INTENTS_VERSION] +} export const CONTRACT_VERSION_ABI = [ { type: "function", @@ -28,19 +39,33 @@ async function readContractVersion(client: PublicClient, address: HexString): Pr } } -/** Whether the gateway reports the supported release. RPC failures propagate. */ -export async function supportsRateFills(client: PublicClient, gateway: HexString): Promise { - return (await readContractVersion(client, gateway)) === SUPPORTED_INTENTS_VERSION +/** Whether the gateway on `stateMachineId` reports a supported release. RPC failures propagate. */ +export async function supportsRateFills( + client: PublicClient, + gateway: HexString, + stateMachineId: string, +): Promise { + const version = await readContractVersion(client, gateway) + return supportedReleases(stateMachineId).some((release) => release === version) } -/** The gateway must report release {@link SUPPORTED_INTENTS_VERSION}; a missing getter or any other release throws. */ -export async function assertGatewayRelease(client: PublicClient, gateway: HexString): Promise { +/** + * The gateway on `stateMachineId` must report a supported release: {@link SUPPORTED_INTENTS_VERSION}, or + * 3 on a testnet. A missing getter or any other release throws. + */ +export async function assertGatewayRelease( + client: PublicClient, + gateway: HexString, + stateMachineId: string, +): Promise { + const supported = supportedReleases(stateMachineId) const version = await readContractVersion(client, gateway) - if (version === SUPPORTED_INTENTS_VERSION) return + if (supported.some((release) => release === version)) return + const required = supported.join(" or ") throw new Error( version === undefined - ? `IntentGateway ${gateway} reports no version(); this SDK requires release ${SUPPORTED_INTENTS_VERSION}` - : `IntentGateway ${gateway} reports release ${String(version)}; this SDK requires release ${SUPPORTED_INTENTS_VERSION}`, + ? `IntentGateway ${gateway} reports no version(); this SDK requires release ${required}` + : `IntentGateway ${gateway} reports release ${String(version)}; this SDK requires release ${required}`, ) } diff --git a/sdk/packages/sdk/src/tests/fillOrderCodec.test.ts b/sdk/packages/sdk/src/tests/fillOrderCodec.test.ts index cb48265fb..e05bcc845 100644 --- a/sdk/packages/sdk/src/tests/fillOrderCodec.test.ts +++ b/sdk/packages/sdk/src/tests/fillOrderCodec.test.ts @@ -12,6 +12,8 @@ import { import type { FillOptions, HexString, Order, TokenInfo } from "@/types" const GATEWAY = "0x1111111111111111111111111111111111111111" as HexString +const MAINNET = "EVM-8453" +const TESTNET = "EVM-97" const TOKEN = "0x0000000000000000000000000000000000000000000000000000000000000002" as HexString /** The pre-quote `fillOrder(Order, (relayerFee, nativeDispatchFee, validUntil, outputs))`, selector `0xa5470064`. */ @@ -127,49 +129,80 @@ describe("decodeFillOrder", () => { }) describe("assertGatewayRelease", () => { - it("accepts release 3", async () => { - await expect(assertGatewayRelease(client(vi.fn().mockResolvedValue(3n)), GATEWAY)).resolves.toBeUndefined() + it("accepts release 4", async () => { + await expect( + assertGatewayRelease(client(vi.fn().mockResolvedValue(4n)), GATEWAY, MAINNET), + ).resolves.toBeUndefined() }) - it.each([0n, 1n, 2n, 4n, 5n, (1n << 64n) - 1n])("rejects release %s", async (release) => { - await expect(assertGatewayRelease(client(vi.fn().mockResolvedValue(release)), GATEWAY)).rejects.toThrow( - /release 3/, - ) + it.each([0n, 1n, 2n, 3n, 5n, (1n << 64n) - 1n])("rejects release %s", async (release) => { + await expect( + assertGatewayRelease(client(vi.fn().mockResolvedValue(release)), GATEWAY, MAINNET), + ).rejects.toThrow(/release 4/) }) it("rejects a malformed version instead of guessing", async () => { - await expect(assertGatewayRelease(client(vi.fn().mockResolvedValue("0x12345678")), GATEWAY)).rejects.toThrow( - /release 3/, - ) + await expect( + assertGatewayRelease(client(vi.fn().mockResolvedValue("0x12345678")), GATEWAY, MAINNET), + ).rejects.toThrow(/release 4/) }) it("is never cached, so an upgrade is seen on the next read", async () => { - const c = client(vi.fn().mockResolvedValue(2n)) - await expect(assertGatewayRelease(c, GATEWAY)).rejects.toThrow(/release 2/) - c.readContract.mockResolvedValue(3n) - await expect(assertGatewayRelease(c, GATEWAY)).resolves.toBeUndefined() + const c = client(vi.fn().mockResolvedValue(3n)) + await expect(assertGatewayRelease(c, GATEWAY, MAINNET)).rejects.toThrow(/release 3/) + c.readContract.mockResolvedValue(4n) + await expect(assertGatewayRelease(c, GATEWAY, MAINNET)).resolves.toBeUndefined() expect(c.readContract).toHaveBeenCalledTimes(2) }) }) +describe("testnet gateways", () => { + // Testnet runs release-4 code but reports 3, so a testnet chain accepts both. + it.each([3n, 4n])("accepts release %s on a testnet chain", async (release) => { + await expect( + assertGatewayRelease(client(vi.fn().mockResolvedValue(release)), GATEWAY, TESTNET), + ).resolves.toBeUndefined() + await expect(supportsRateFills(client(vi.fn().mockResolvedValue(release)), GATEWAY, TESTNET)).resolves.toBe( + true, + ) + }) + + it.each([0n, 1n, 2n, 5n])("rejects release %s on a testnet chain", async (release) => { + await expect( + assertGatewayRelease(client(vi.fn().mockResolvedValue(release)), GATEWAY, TESTNET), + ).rejects.toThrow(/release 3 or 4/) + await expect(supportsRateFills(client(vi.fn().mockResolvedValue(release)), GATEWAY, TESTNET)).resolves.toBe( + false, + ) + }) + + it("still requires release 4 on mainnet", async () => { + await expect(assertGatewayRelease(client(vi.fn().mockResolvedValue(3n)), GATEWAY, MAINNET)).rejects.toThrow( + /requires release 4$/, + ) + }) +}) + describe("supportsRateFills", () => { it("reads only the gateway release", async () => { - const readContract = vi.fn().mockResolvedValue(3n) - await expect(supportsRateFills(client(readContract), GATEWAY)).resolves.toBe(true) + const readContract = vi.fn().mockResolvedValue(4n) + await expect(supportsRateFills(client(readContract), GATEWAY, MAINNET)).resolves.toBe(true) expect(readContract).toHaveBeenCalledTimes(1) expect(readContract.mock.calls[0][0].address).toBe(GATEWAY) }) - it.each([0n, 1n, 2n, 4n, 5n, (1n << 64n) - 1n])("rejects gateway release %s", async (release) => { - await expect(supportsRateFills(client(vi.fn().mockResolvedValue(release)), GATEWAY)).resolves.toBe(false) + it.each([0n, 1n, 2n, 3n, 5n, (1n << 64n) - 1n])("rejects gateway release %s", async (release) => { + await expect(supportsRateFills(client(vi.fn().mockResolvedValue(release)), GATEWAY, MAINNET)).resolves.toBe( + false, + ) }) it("does not cache capability across calls", async () => { - const c = client(vi.fn().mockResolvedValue(3n)) + const c = client(vi.fn().mockResolvedValue(4n)) - expect(await supportsRateFills(c, GATEWAY)).toBe(true) - c.readContract.mockResolvedValue(2n) - expect(await supportsRateFills(c, GATEWAY)).toBe(false) + expect(await supportsRateFills(c, GATEWAY, MAINNET)).toBe(true) + c.readContract.mockResolvedValue(3n) + expect(await supportsRateFills(c, GATEWAY, MAINNET)).toBe(false) expect(c.readContract).toHaveBeenCalledTimes(2) }) }) @@ -194,8 +227,8 @@ describe("version() failures with real viem errors", () => { { code: -32005, message: "rate limit exceeded" }, ])("propagates provider failure $code: $message", async (rpcError) => { const c = rpcClient(rpcError) - await expect(assertGatewayRelease(c, GATEWAY)).rejects.toThrow(rpcError.message) - await expect(supportsRateFills(c, GATEWAY)).rejects.toThrow(rpcError.message) + await expect(assertGatewayRelease(c, GATEWAY, MAINNET)).rejects.toThrow(rpcError.message) + await expect(supportsRateFills(c, GATEWAY, MAINNET)).rejects.toThrow(rpcError.message) }) it.each([ @@ -205,13 +238,13 @@ describe("version() failures with real viem errors", () => { { code: -32000, message: "function selector was not recognized and there's no fallback function" }, ])("treats a genuine EVM failure $code: $message as a missing getter", async (rpcError) => { const c = rpcClient(rpcError) - await expect(assertGatewayRelease(c, GATEWAY)).rejects.toThrow(/no version\(\)/) - await expect(supportsRateFills(c, GATEWAY)).resolves.toBe(false) + await expect(assertGatewayRelease(c, GATEWAY, MAINNET)).rejects.toThrow(/no version\(\)/) + await expect(supportsRateFills(c, GATEWAY, MAINNET)).resolves.toBe(false) }) it("treats a successful call returning no data as a missing getter", async () => { const c = rpcClient() - await expect(assertGatewayRelease(c, GATEWAY)).rejects.toThrow(/no version\(\)/) - await expect(supportsRateFills(c, GATEWAY)).resolves.toBe(false) + await expect(assertGatewayRelease(c, GATEWAY, MAINNET)).rejects.toThrow(/no version\(\)/) + await expect(supportsRateFills(c, GATEWAY, MAINNET)).resolves.toBe(false) }) }) diff --git a/sdk/packages/simplex/filler-config-example.toml b/sdk/packages/simplex/filler-config-example.toml index 89f131886..4be6e2e2d 100644 --- a/sdk/packages/simplex/filler-config-example.toml +++ b/sdk/packages/simplex/filler-config-example.toml @@ -191,7 +191,7 @@ triggerPercentage = 0.5 # referenced by symbol from Simplex's built-in registry — no address # configuration needed. The registry ships USDC, USDT, DAI, CNGN (per-chain via # the SDK, testnets included) plus curated mainnet deployments of USDR, ZARP, -# EURC, XSGD and TRYB, all verified on-chain. +# EURC, XSGD, TRYB and BRIDGE, all verified on-chain. # # One orientation per market: declaring both USDC/CNGN and CNGN/USDC is refused. # token0 == token1 is the same-asset cross-chain market. diff --git a/sdk/packages/simplex/src/config/asset-registry.ts b/sdk/packages/simplex/src/config/asset-registry.ts index 72532590b..83a953b88 100644 --- a/sdk/packages/simplex/src/config/asset-registry.ts +++ b/sdk/packages/simplex/src/config/asset-registry.ts @@ -36,7 +36,8 @@ interface BuiltinSpec { * so a new asset is added there once and never in a parallel table here. * ZARP/EURC/XSGD/TRYB/USDR are curated stablecoin deployments whose addresses * were taken from the issuer's official documentation and verified on-chain - * (`symbol()`/`decimals()`) before inclusion in the SDK registry. + * (`symbol()`/`decimals()`) before inclusion in the SDK registry. BRIDGE is + * Hyperbridge's own BridgeToken, on BNB Chain and Polygon. */ const BUILTIN_ASSETS: Record = { USDC: { resolve: (r, chain) => r.getUsdcAsset(chain) }, @@ -48,6 +49,7 @@ const BUILTIN_ASSETS: Record = { EURC: { resolve: (r, chain) => r.getAssetBySymbol(chain, "EURC") }, XSGD: { resolve: (r, chain) => r.getAssetBySymbol(chain, "XSGD") }, TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") }, + BRIDGE: { resolve: (r, chain) => r.getAssetBySymbol(chain, "BRIDGE") }, } /** @@ -131,10 +133,10 @@ export function validateAssetDefinitions(assets: Record * 1. the user's `[assets]` table — an *escape hatch* for assets the registry * doesn't ship (or per-deployment overrides), never required for shipped * symbols; - * 2. shipped symbols (USDC, USDT, DAI, CNGN, USDR, ZARP, EURC, XSGD, TRYB) - * resolved per chain from the SDK chain registry (`chain.ts`) — the single - * source of truth shared with the rest of the SDK, so an address - * correction there is never shadowed by a parallel table here. + * 2. shipped symbols (USDC, USDT, DAI, CNGN, USDR, ZARP, EURC, XSGD, TRYB, + * BRIDGE) resolved per chain from the SDK chain registry (`chain.ts`) — + * the single source of truth shared with the rest of the SDK, so an + * address correction there is never shadowed by a parallel table here. * * Address lookups are per `(symbol, chain)` — a chain where no layer knows the * asset simply doesn't trade pairs involving it. The registry holds addresses diff --git a/sdk/packages/simplex/src/config/filler-toml.ts b/sdk/packages/simplex/src/config/filler-toml.ts index afe2bf861..b9a1e12db 100644 --- a/sdk/packages/simplex/src/config/filler-toml.ts +++ b/sdk/packages/simplex/src/config/filler-toml.ts @@ -68,7 +68,7 @@ export interface FillerTomlConfig { * Optional asset-registry escape hatch: symbol → { chain → address }. Only * needed for assets the built-in registry does not ship, or to override a * shipped address for a deployment. Shipped symbols: USDC, USDT, DAI, CNGN - * (SDK chain registry) + curated USDR, ZARP, EURC, XSGD and TRYB. + * (SDK chain registry) + curated USDR, ZARP, EURC, XSGD, TRYB and BRIDGE. */ assets?: Record /** diff --git a/sdk/packages/simplex/src/tests/helpers/posting.ts b/sdk/packages/simplex/src/tests/helpers/posting.ts index f046fd84c..dc0052b52 100644 --- a/sdk/packages/simplex/src/tests/helpers/posting.ts +++ b/sdk/packages/simplex/src/tests/helpers/posting.ts @@ -16,10 +16,10 @@ import type { Signer } from "@/services/wallet" */ /** The gateway release a fill settles on, which `prepareSubmitBid` insists both sides report. */ -const SUPPORTED_RELEASE = 3n +const SUPPORTED_RELEASE = 4n /** Stands in for Base's deployed SolverAccount, the implementation the release check reads. */ -const SOLVER_ACCOUNT = "0x77c3394CA5881A74f18139AC87D0c11F8Faa90cC" as HexString +const SOLVER_ACCOUNT = "0xaAd062555800a97Af062795189e32a3CBd045612" as HexString /** Anvil's first account, which is the key the orderbook's vectors were signed with. */ export const SOLVER_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as HexString @@ -41,7 +41,7 @@ function intentGateway(chainId: number): Promise { // biome-ignore lint/suspicious/noExplicitAny: the fee token read is the one thing here that wants a node ;(chain as any).getFeeTokenWithDecimals = async () => ({ address: BASE_USDC, decimals: 6 }) // Signing a bid reads `version()` off the gateway and the solver account, since - // a fill settles only on release 3. Both answer it here: the rig is about the + // a fill settles only on release 4. Both answer it here: the rig is about the // bytes the op carries, and there is no node to ask. // biome-ignore lint/suspicious/noExplicitAny: the stubs below stand in for a node const stubbed = chain as any diff --git a/sdk/packages/simplex/src/tests/pairs.test.ts b/sdk/packages/simplex/src/tests/pairs.test.ts index 3a1566767..c769f7cdc 100644 --- a/sdk/packages/simplex/src/tests/pairs.test.ts +++ b/sdk/packages/simplex/src/tests/pairs.test.ts @@ -124,9 +124,19 @@ describe("AssetRegistry", () => { // every cNGN quote on that chain by 1e12. expect(sdk.getCNgnDecimals("EVM-56")).toBe(6) expect(sdk.getUsdcDecimals("EVM-56")).toBe(18) + // BRIDGE is the same CREATE2 address on both of its chains, with OpenZeppelin's ERC20 layout. + for (const chain of ["EVM-56", "EVM-137"]) { + expect(registry.getAddress("BRIDGE", chain)).toBe("0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB") + expect(sdk.getAssetMetadataBySymbol(chain, "bridge")?.decimals).toBe(18) + expect(sdk.getTokenStorageSlots(chain, "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB")).toEqual({ + balanceSlot: 0, + allowanceSlot: 1, + }) + } // Not deployed there → absent, not an error. expect(registry.getAddress("EURC", "EVM-56")).toBeNull() expect(registry.getAddress("USDR", "EVM-42161")).toBeNull() + expect(registry.getAddress("BRIDGE", "EVM-1")).toBeNull() }) it("rejects malformed definitions", () => {