-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add a.DI helper #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity >=0.8.0; | ||
|
|
||
| /// library imports | ||
| import "forge-std/Test.sol"; | ||
|
|
||
| /// local imports | ||
| import {CcipHelper} from "../ccip/CcipHelper.sol"; | ||
| import {LayerZeroV2Helper} from "../layerzero-v2/LayerZeroV2Helper.sol"; | ||
| import {HyperlaneHelper} from "../hyperlane/HyperlaneHelper.sol"; | ||
| import {ArbitrumNativeHelper} from "../arbitrum/ArbitrumNativeHelper.sol"; | ||
|
|
||
| /// @title a.DI Helper | ||
| /// @notice Helps simulate Aave Delivery Infrastructure (a.DI) envelope flows by composing pigeon's existing | ||
| /// per-AMB helpers (CCIP, LayerZero V2, Hyperlane) and a new Arbitrum-native primitive. | ||
| /// @dev a.DI's `CrossChainForwarder.forwardMessage` broadcasts an envelope to a (possibly shuffled) subset of | ||
| /// configured bridge adapters; the destination CCC executes the receiver once `requiredConfirmation` adapters | ||
| /// have delivered. This helper does not assume which adapters fired — each child helper self-filters its own | ||
| /// AMB's events and silently no-ops if none appear. | ||
| /// @dev IMPORTANT: a.DI's CCC must hold native to pay AMB fees. Callers MUST `vm.deal(address(CCC), ...)` BEFORE | ||
| /// invoking `forwardMessage`. This helper does NOT fund the CCC. | ||
| contract AdiHelper is Test { | ||
| CcipHelper public immutable ccipHelper; | ||
| LayerZeroV2Helper public immutable lzHelper; | ||
| HyperlaneHelper public immutable hlHelper; | ||
| ArbitrumNativeHelper public immutable arbHelper; | ||
|
|
||
| /// @dev keccak256("TransactionForwardingAttempted(bytes32,bytes32,bytes,uint256,address,address,bool,bytes)") | ||
| bytes32 public constant TRANSACTION_FORWARDING_ATTEMPTED_SELECTOR = | ||
| 0x935aa87d643578e6395c90fdbd5d50ffee5f2c1f6ce2cd01274740412bb679f4; | ||
|
|
||
| struct EthToArbArgs { | ||
| uint256 l2ForkId; | ||
| address l1Inbox; // 0 = any | ||
| address l1Bridge; // 0 = any | ||
| address expectedL1CCC; // expected L1 sender on the retryable (CCC due to delegatecall) | ||
| Vm.Log[] logs; | ||
| } | ||
|
|
||
| /// @notice Args for any "multi-bridge consensus" lane (Arb→Eth in v1; future Arb→Op etc.). | ||
| /// @dev Set any endpoint/router to address(0) to disable that AMB. Each child helper self-filters. | ||
| struct MultiBridgeArgs { | ||
| uint256 dstForkId; | ||
| // CCIP | ||
| address dstCcipRouter; // 0 disables CCIP relay | ||
| uint64 dstCcipChainSelector; // 0 = no selector filter | ||
| address srcCcipOnRamp; // 0 = no emitter filter | ||
| // LayerZero V2 | ||
| address dstLzEndpoint; // 0 disables LZ relay | ||
| // Hyperlane | ||
| address srcHlMailbox; // 0 disables HL relay (HyperlaneHelper requires both) | ||
| address dstHlMailbox; // 0 disables HL relay | ||
| Vm.Log[] logs; | ||
| } | ||
|
|
||
| constructor(CcipHelper c, LayerZeroV2Helper l, HyperlaneHelper h, ArbitrumNativeHelper a) { | ||
| ccipHelper = c; | ||
| lzHelper = l; | ||
| hlHelper = h; | ||
| arbHelper = a; | ||
| vm.makePersistent(address(this)); | ||
| vm.makePersistent(address(c)); | ||
| vm.makePersistent(address(l)); | ||
| vm.makePersistent(address(h)); | ||
| vm.makePersistent(address(a)); | ||
| } | ||
|
|
||
| ////////////////////////////////////////////////////////////// | ||
| // EXTERNAL FUNCTIONS // | ||
| ////////////////////////////////////////////////////////////// | ||
|
|
||
| /// @notice Relay an Eth → Arb a.DI envelope via the Arbitrum native bridge. | ||
| /// @dev Caller must `vm.deal(L1_CCC, ...)` BEFORE `forwardMessage` to fund the retryable. | ||
| /// @param args the relay arguments | ||
| function helpEthToArb(EthToArbArgs memory args) external { | ||
| ArbitrumNativeHelper.HelpArgs memory inner = ArbitrumNativeHelper.HelpArgs({ | ||
| l2ForkId: args.l2ForkId, | ||
| l1Inbox: args.l1Inbox, | ||
| l1Bridge: args.l1Bridge, | ||
| expectedL1Sender: args.expectedL1CCC, | ||
| logs: args.logs | ||
| }); | ||
| arbHelper.help(inner); | ||
| } | ||
|
|
||
| /// @notice Relay any multi-bridge consensus a.DI envelope (Arb → Eth canonical lane; future Arb → Op). | ||
| /// @dev Each child helper self-filters from `args.logs`. Setting an endpoint to address(0) skips that AMB. | ||
| /// @dev Over-delivery is fine: once the destination CCC's threshold is met the envelope transitions to | ||
| /// `Delivered`, and subsequent adapter deliveries just increment `confirmations` without re-executing the | ||
| /// receiver. The receive path through each adapter must succeed though — the per-adapter `onlyMailBox` / | ||
| /// `onlyEndpoint` / `onlyRouter` checks must match the prank target you pass in. Read each deployed adapter's | ||
| /// configured AMB endpoint via its public getter (e.g., `HL_MAIL_BOX()`, `LZ_ENDPOINT()`, `getRouter()`) — do | ||
| /// NOT hardcode canonical AMB addresses, since deployments may use custom AMB infrastructure. | ||
| /// @param args the relay arguments | ||
| function helpMultiBridge(MultiBridgeArgs memory args) external { | ||
| if (args.dstCcipRouter != address(0)) { | ||
| CcipHelper.HelpArgs memory ccipArgs = CcipHelper.HelpArgs({ | ||
| dstForkId: args.dstForkId, | ||
| dstRouter: args.dstCcipRouter, | ||
| expDstChainSelector: args.dstCcipChainSelector, | ||
| srcOnRamp: args.srcCcipOnRamp, | ||
| logs: args.logs | ||
| }); | ||
| ccipHelper.help(ccipArgs); | ||
| } | ||
| if (args.dstLzEndpoint != address(0)) { | ||
| lzHelper.help(args.dstLzEndpoint, args.dstForkId, args.logs); | ||
| } | ||
| if (args.srcHlMailbox != address(0) && args.dstHlMailbox != address(0)) { | ||
| hlHelper.help(args.srcHlMailbox, args.dstHlMailbox, args.dstForkId, args.logs); | ||
| } | ||
| } | ||
|
|
||
| /// @notice Count source-side `TransactionForwardingAttempted` events with `adapterSuccessful = true`. | ||
| /// @dev Useful for tests that want to assert the shuffle picked >= N adapters and they succeeded. | ||
| /// @param logs the recorded source-tx logs | ||
| /// @return count number of successful forwarding attempts | ||
| function countSuccessfulForwards(Vm.Log[] memory logs) external pure returns (uint256 count) { | ||
| for (uint256 i; i < logs.length; ++i) { | ||
| if (logs[i].topics.length < 4) continue; | ||
| if (logs[i].topics[0] != TRANSACTION_FORWARDING_ATTEMPTED_SELECTOR) continue; | ||
| // adapterSuccessful is the third indexed field; topic[3] = bytes32(uint256(1)) when true | ||
| if (logs[i].topics[3] == bytes32(uint256(1))) ++count; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity >=0.8.0; | ||
|
|
||
| /// library imports | ||
| import "forge-std/Test.sol"; | ||
|
|
||
| /// @title Arbitrum Native Helper | ||
| /// @notice Helps simulate L1 → Arbitrum native-bridge retryable-ticket delivery in forked tests. | ||
| /// @dev Detects `IBridge.MessageDelivered` + `IDelayedMessageProvider.InboxMessageDelivered` events on L1 | ||
| /// (paired by `messageNum`), decodes the packed retryable payload to `(to, data)`, switches to the L2 fork, | ||
| /// pranks `MessageDelivered.sender` (already aliased by the Inbox), and calls `to.call(data)`. Reusable | ||
| /// beyond a.DI for any L1 contract that wraps `Inbox.createRetryableTicket`. | ||
| /// @dev Note on aliasing: `AbsInbox._submitRetryable` calls `applyL1ToL2Alias(msg.sender)` BEFORE delivering | ||
| /// to the Bridge, so `MessageDelivered.sender` is already the L2 alias. The helper does NOT re-alias. | ||
| contract ArbitrumNativeHelper is Test { | ||
| /// @dev keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)") | ||
| bytes32 public constant MESSAGE_DELIVERED_SELECTOR = | ||
| 0x5e3c1311ea442664e8b1611bfabef659120ea7a0a2cfc0667700bebc69cbffe1; | ||
|
|
||
| /// @dev keccak256("InboxMessageDelivered(uint256,bytes)") | ||
| bytes32 public constant INBOX_MESSAGE_DELIVERED_SELECTOR = | ||
| 0xff64905f73a67fb594e0f940a8075a860db489ad991e032f48c81123eb52d60b; | ||
|
|
||
| /// @dev L1 → L2 address aliasing offset | ||
| uint160 public constant ALIAS_OFFSET = uint160(0x1111000000000000000000000000000000001111); | ||
|
|
||
| /// @dev `MessageDelivered.kind` value for retryable submissions (`L1MessageType_submitRetryableTx`). | ||
| uint8 public constant L1_MESSAGE_TYPE_RETRYABLE = 9; | ||
|
|
||
| struct HelpArgs { | ||
| uint256 l2ForkId; // destination Arbitrum fork id | ||
| address l1Inbox; // optional emitter filter for InboxMessageDelivered (0 = any) | ||
| address l1Bridge; // optional emitter filter for MessageDelivered (0 = any) | ||
| address expectedL1Sender; // optional raw L1 sender filter (0 = any). Compared via applyL1ToL2Alias. | ||
| Vm.Log[] logs; // logs from vm.recordLogs on L1 | ||
| } | ||
|
|
||
| error MessageDeliveredMissing(uint256 messageNum); | ||
| error RetryableCallFailed(bytes returnData); | ||
| error MalformedRetryablePayload(); | ||
|
|
||
| mapping(uint256 => bool) internal _processedMessageNums; | ||
|
|
||
| ////////////////////////////////////////////////////////////// | ||
| // EXTERNAL FUNCTIONS // | ||
| ////////////////////////////////////////////////////////////// | ||
|
|
||
| /// @notice helps relay one or more L1 → Arbitrum retryables from recorded logs | ||
| /// @param args the relay arguments | ||
| function help(HelpArgs memory args) external { | ||
| _help(args); | ||
| } | ||
|
|
||
| /// @notice filter logs to those matching `InboxMessageDelivered` | ||
| /// @param logs the recorded logs | ||
| /// @param length the maximum number of matching logs to return | ||
| /// @return found array of matching logs, sized to the actual number found (≤ length) | ||
| function findLogs(Vm.Log[] calldata logs, uint256 length) external pure returns (Vm.Log[] memory found) { | ||
| found = new Vm.Log[](length); | ||
| uint256 idx; | ||
| for (uint256 i; i < logs.length; ++i) { | ||
| if (logs[i].topics.length == 0) continue; | ||
| if (logs[i].topics[0] == INBOX_MESSAGE_DELIVERED_SELECTOR) { | ||
| found[idx++] = logs[i]; | ||
| if (idx == length) break; | ||
| } | ||
| } | ||
| // shrink array length to the actual match count so trailing zero entries aren't returned | ||
| assembly { | ||
| mstore(found, idx) | ||
| } | ||
| } | ||
|
|
||
| /// @notice compute the L2 alias of an L1 address | ||
| function applyL1ToL2Alias(address l1) public pure returns (address) { | ||
| unchecked { | ||
| return address(uint160(l1) + ALIAS_OFFSET); | ||
| } | ||
| } | ||
|
|
||
| ////////////////////////////////////////////////////////////// | ||
| // INTERNAL FUNCTIONS // | ||
| ////////////////////////////////////////////////////////////// | ||
|
|
||
| /// @notice scan logs for retryables and relay each to the L2 fork | ||
| function _help(HelpArgs memory args) internal { | ||
| uint256 prevForkId = vm.activeFork(); | ||
|
|
||
| for (uint256 i; i < args.logs.length; ++i) { | ||
| Vm.Log memory l = args.logs[i]; | ||
| if (l.topics.length < 2) continue; | ||
| if (l.topics[0] != INBOX_MESSAGE_DELIVERED_SELECTOR) continue; | ||
| if (args.l1Inbox != address(0) && l.emitter != args.l1Inbox) continue; | ||
|
|
||
| uint256 messageNum = uint256(l.topics[1]); | ||
| if (_processedMessageNums[messageNum]) continue; | ||
|
|
||
| (bool foundPair, address aliasedSender, uint8 kind) = | ||
| _findPairedMessageDelivered(args.logs, messageNum, args.l1Bridge); | ||
| if (!foundPair) revert MessageDeliveredMissing(messageNum); | ||
| if (kind != L1_MESSAGE_TYPE_RETRYABLE) continue; | ||
| if ( | ||
| args.expectedL1Sender != address(0) | ||
| && aliasedSender != applyL1ToL2Alias(args.expectedL1Sender) | ||
| ) continue; | ||
|
|
||
| bytes memory payload = abi.decode(l.data, (bytes)); | ||
| (address to, bytes memory innerData) = _decodeRetryablePayload(payload); | ||
|
|
||
| _processedMessageNums[messageNum] = true; | ||
|
|
||
| vm.selectFork(args.l2ForkId); | ||
| vm.prank(aliasedSender, aliasedSender); | ||
| (bool ok, bytes memory ret) = to.call(innerData); | ||
| if (!ok) revert RetryableCallFailed(ret); | ||
| } | ||
|
|
||
| vm.selectFork(prevForkId); | ||
| } | ||
|
|
||
| /// @notice locate the `MessageDelivered` event paired with a given `messageNum` | ||
| /// @return found whether a paired event was found | ||
| /// @return aliasedSender the already-aliased L1 sender stored by the Inbox | ||
| /// @return kind the message kind (9 for retryables) | ||
| function _findPairedMessageDelivered(Vm.Log[] memory logs, uint256 messageNum, address l1Bridge) | ||
| internal | ||
| pure | ||
| returns (bool found, address aliasedSender, uint8 kind) | ||
| { | ||
| for (uint256 i; i < logs.length; ++i) { | ||
| Vm.Log memory l = logs[i]; | ||
| if (l.topics.length < 2) continue; | ||
| if (l.topics[0] != MESSAGE_DELIVERED_SELECTOR) continue; | ||
| if (l1Bridge != address(0) && l.emitter != l1Bridge) continue; | ||
| if (uint256(l.topics[1]) != messageNum) continue; | ||
|
|
||
| // MessageDelivered.data = abi.encode(inbox, kind, sender, messageDataHash, baseFeeL1, timestamp) | ||
| (, uint8 _kind, address _sender,,,) = | ||
| abi.decode(l.data, (address, uint8, address, bytes32, uint256, uint64)); | ||
| return (true, _sender, _kind); | ||
| } | ||
| return (false, address(0), 0); | ||
| } | ||
|
|
||
| /// @notice decode the abi-packed retryable payload from `Inbox.createRetryableTicket` | ||
| /// @dev Layout (from `nitro-contracts/AbsInbox._submitRetryable`): | ||
| /// uint256(to) | l2CallValue | msg.value | maxSubmissionCost | | ||
| /// uint256(excessFeeRefundAddress) | uint256(callValueRefundAddress) | | ||
| /// gasLimit | maxFeePerGas | uint256(callDataLength) | data | ||
| function _decodeRetryablePayload(bytes memory payload) internal pure returns (address to, bytes memory data) { | ||
| if (payload.length < 9 * 32) revert MalformedRetryablePayload(); | ||
|
|
||
| uint256 toWord; | ||
| uint256 callDataLength; | ||
| assembly { | ||
| toWord := mload(add(payload, 32)) | ||
| callDataLength := mload(add(payload, mul(32, 9))) | ||
| } | ||
|
|
||
| if (payload.length < 9 * 32 + callDataLength) revert MalformedRetryablePayload(); | ||
| to = address(uint160(toWord)); | ||
|
|
||
| data = new bytes(callDataLength); | ||
| for (uint256 i; i < callDataLength; ++i) { | ||
| data[i] = payload[9 * 32 + i]; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.