From 0d3b314137872cd3e665e18a424e0374355d8d8a Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 11 May 2026 18:05:10 +0400 Subject: [PATCH 01/69] feat: swap bridge test --- src/dummyRouter.sol | 28 +-- .../AcrossERC20AmountManipulator.sol | 46 +---- test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 169 ++++++++++++++++++ 3 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 test/poc/OpenOceanAcrossOpenRouterPoC.t.sol diff --git a/src/dummyRouter.sol b/src/dummyRouter.sol index 043f24d..e0b5ef9 100644 --- a/src/dummyRouter.sol +++ b/src/dummyRouter.sol @@ -2,7 +2,6 @@ pragma solidity =0.8.25; contract DummyRouter { - enum CallType { CALL, STATICCALL @@ -10,19 +9,22 @@ contract DummyRouter { struct Splice { uint256 sourceActionIndex; // which previous return data to read from - uint256 srcOffset; // offset inside previous returndata - uint256 dstOffset; // offset inside current calldata - uint256 length; // bytes to copy + uint256 srcOffset; // offset inside previous returndata + uint256 dstOffset; // offset inside current calldata + uint256 length; // bytes to copy } struct Action { CallType callType; address target; - uint256 value; bytes data; Splice[] splices; } + error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); + error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); + error CallFailed(uint256 actionIndex, bytes returndata); + function execute(Action[] calldata actions) external payable returns (bytes[] memory results) { results = new bytes[](actions.length); @@ -32,16 +34,16 @@ contract DummyRouter { // Patch this action's calldata using earlier action results. for (uint256 j = 0; j < actions[i].splices.length; j++) { Splice calldata s = actions[i].splices[j]; + if (s.sourceActionIndex >= i) revert FutureSplice(i, s.sourceActionIndex); bytes memory source = results[s.sourceActionIndex]; + if (s.srcOffset + s.length > source.length || s.dstOffset + s.length > callData.length) { + revert SpliceOutOfBounds(i, j); + } - _copyBytes({ - src: source, - dst: callData, - srcOffset: s.srcOffset, - dstOffset: s.dstOffset, - length: s.length - }); + for (uint256 k = 0; k < s.length; k++) { + callData[s.dstOffset + k] = source[s.srcOffset + k]; + } } bool success; @@ -50,7 +52,7 @@ contract DummyRouter { if (actions[i].callType == CallType.STATICCALL) { (success, ret) = actions[i].target.staticcall(callData); } else { - (success, ret) = actions[i].target.call{value: actions[i].value}(callData); + (success, ret) = actions[i].target.call(callData); } if (!success) revert CallFailed(i, ret); diff --git a/src/manipulators/AcrossERC20AmountManipulator.sol b/src/manipulators/AcrossERC20AmountManipulator.sol index 783e32b..d99d79d 100644 --- a/src/manipulators/AcrossERC20AmountManipulator.sol +++ b/src/manipulators/AcrossERC20AmountManipulator.sol @@ -1,60 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.19; +pragma solidity =0.8.25; -import {ERC20} from "solady/src/tokens/ERC20.sol"; - -/// @title AcrossERC20AmountManipulator -/// @notice Stateless helper for OpenRouter-style batches that need Across deposit amounts after swap and fee transfer +/// @notice Computes the Across output amount that must be spliced into SpokePool.deposit calldata. contract AcrossERC20AmountManipulator { - error InvalidAddress(); error BridgeFeeExceedsInputAmount(); error DecimalDiffTooLarge(); uint256 internal constant MAX_SAFE_DECIMAL_DIFF = 77; - /// @notice Reads the current ERC20 balance and derives Across input/output amounts. - /// @dev Intended to be called after swap and fee-transfer actions have completed. - /// Returndata layout is two ABI words: - /// - offset 0: inputAmount - /// - offset 32: outputAmount - /// @param token ERC20 token to bridge. - /// @param balanceHolder Address whose post-fee balance should be used as Across inputAmount. - /// @param bridgeFee Fee to subtract before deriving outputAmount, denominated in input token decimals. - /// @param inputTokenDecimals Decimals of the source/input token. - /// @param outputTokenDecimals Decimals of the destination/output token. - function acrossAmounts( - address token, - address balanceHolder, - uint256 bridgeFee, - uint256 inputTokenDecimals, - uint256 outputTokenDecimals - ) external view returns (uint256 inputAmount, uint256 outputAmount) { - if (token == address(0) || balanceHolder == address(0)) revert InvalidAddress(); - - inputAmount = ERC20(token).balanceOf(balanceHolder); - outputAmount = deriveOutputAmount(inputAmount, bridgeFee, inputTokenDecimals, outputTokenDecimals); - } - - /// @notice Derives Across input/output amounts from a caller-provided input amount. - /// @dev Use this when a previous OpenRouter action already returned the final post-fee amount. - function acrossAmountsFromInput( - uint256 inputAmount, - uint256 bridgeFee, - uint256 inputTokenDecimals, - uint256 outputTokenDecimals - ) external pure returns (uint256, uint256 outputAmount) { - outputAmount = deriveOutputAmount(inputAmount, bridgeFee, inputTokenDecimals, outputTokenDecimals); - return (inputAmount, outputAmount); - } - - /// @notice Derives Across outputAmount from a runtime inputAmount. /// @dev bridgeFee is denominated in input token decimals. function deriveOutputAmount( uint256 inputAmount, uint256 bridgeFee, uint256 inputTokenDecimals, uint256 outputTokenDecimals - ) public pure returns (uint256 outputAmount) { + ) external pure returns (uint256 outputAmount) { if (bridgeFee > inputAmount) revert BridgeFeeExceedsInputAmount(); uint256 amountAfterFee = inputAmount - bridgeFee; diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol new file mode 100644 index 0000000..624f9a9 --- /dev/null +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {DummyRouter} from "../../src/dummyRouter.sol"; +import {AcrossERC20AmountManipulator} from "../../src/manipulators/AcrossERC20AmountManipulator.sol"; + +interface ISpokePool { + function deposit( + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + bytes32 exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityParameter, + bytes calldata message + ) external payable; +} + +contract OpenOceanAcrossOpenRouterPoCTest is Test { + address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant ACROSS_ARBITRUM_SPOKE_POOL = 0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + address internal constant BASE_WETH = 0x4200000000000000000000000000000000000006; + uint256 internal constant BASE_CHAIN_ID = 8453; + uint256 internal constant FORK_BLOCK_NUMBER = 461_716_058; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01d6d1; + uint256 internal constant SWAP_INPUT_USDC = 0x1640325; + uint256 internal constant DEFAULT_ACROSS_BRIDGE_FEE = 1; + string internal constant OPENOCEAN_SWAP_CALLDATA = + "0x0a9704d5000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a20000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000001640325000000000000000000000000000000000000000000000000002002d5154237f3000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b94700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001640325000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab100000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e8500000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef00020000000000000000000000000000000000000000000000239364a56cb36600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f9900000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + function test_openOceanSwapManipulatorAcrossDeposit_arbitrumFork() public { + string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("ARBITRUM_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + DummyRouter router = _routerAtFixtureAddress(); + AcrossERC20AmountManipulator manipulator = new AcrossERC20AmountManipulator(); + if (bytes(rpcUrl).length == 0) { + emit log("Set ARBITRUM_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_USDC_AMOUNT", SWAP_INPUT_USDC); + uint256 bridgeFee = vm.envOr("POC_ACROSS_BRIDGE_FEE", DEFAULT_ACROSS_BRIDGE_FEE); + + deal(ARBITRUM_USDC, address(router), inputAmount); + deal(ARBITRUM_WETH, address(router), 0); + + DummyRouter.Action[] memory actions = + _buildActions(manipulator, inputAmount, bridgeFee, vm.parseBytes(OPENOCEAN_SWAP_CALLDATA)); + uint256 spokePoolWethBefore = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL); + + bytes[] memory results = router.execute(actions); + + _assertPocResult(router, bridgeFee, spokePoolWethBefore, results[2]); + } + + function _buildActions( + AcrossERC20AmountManipulator manipulator, + uint256 inputAmount, + uint256 bridgeFee, + bytes memory swapCalldata + ) internal view returns (DummyRouter.Action[] memory actions) { + actions = new DummyRouter.Action[](5); + actions[0] = _action( + DummyRouter.CallType.CALL, + ARBITRUM_USDC, + abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), + new DummyRouter.Splice[](0) + ); + + actions[1] = + _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new DummyRouter.Splice[](0)); + + DummyRouter.Splice[] memory outputAmountSplices = new DummyRouter.Splice[](1); + outputAmountSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); + + actions[2] = _action( + DummyRouter.CallType.STATICCALL, + address(manipulator), + abi.encodeCall( + AcrossERC20AmountManipulator.deriveOutputAmount, (uint256(0), bridgeFee, uint256(18), uint256(18)) + ), + outputAmountSplices + ); + + actions[3] = _action( + DummyRouter.CallType.CALL, + ARBITRUM_WETH, + abi.encodeWithSelector(ERC20.approve.selector, ACROSS_ARBITRUM_SPOKE_POOL, type(uint256).max), + new DummyRouter.Splice[](0) + ); + + DummyRouter.Splice[] memory depositSplices = new DummyRouter.Splice[](2); + depositSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 132, length: 32}); + depositSplices[1] = DummyRouter.Splice({sourceActionIndex: 2, srcOffset: 0, dstOffset: 164, length: 32}); + + actions[4] = _action( + DummyRouter.CallType.CALL, ACROSS_ARBITRUM_SPOKE_POOL, _emptyAcrossDepositCalldata(), depositSplices + ); + } + + function _assertPocResult( + DummyRouter router, + uint256 bridgeFee, + uint256 spokePoolWethBefore, + bytes memory manipulatorResult + ) internal view { + uint256 actualInputAmount = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL) - spokePoolWethBefore; + uint256 actualOutputAmount = abi.decode(manipulatorResult, (uint256)); + + assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); + assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), 0); + assertGt(actualInputAmount, 0); + assertEq(actualOutputAmount, actualInputAmount - bridgeFee); + } + + function _routerAtFixtureAddress() internal returns (DummyRouter router) { + DummyRouter implementation = new DummyRouter(); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return DummyRouter(payable(FIXTURE_ROUTER)); + } + + function _emptyAcrossDepositCalldata() internal view returns (bytes memory) { + return abi.encodeWithSelector( + ISpokePool.deposit.selector, + _toBytes32(FIXTURE_RECIPIENT), + _toBytes32(FIXTURE_RECIPIENT), + _toBytes32(ARBITRUM_WETH), + _toBytes32(BASE_WETH), + uint256(0), + uint256(0), + BASE_CHAIN_ID, + bytes32(0), + uint32(block.timestamp), + uint32(block.timestamp + 3 hours), + uint32(0), + "" + ); + } + + function _action( + DummyRouter.CallType callType, + address target, + bytes memory data, + DummyRouter.Splice[] memory splices + ) internal pure returns (DummyRouter.Action memory) { + return DummyRouter.Action({callType: callType, target: target, data: data, splices: splices}); + } + + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} From bbd42dae6aca13a2d3f6825528b31fe0dfd0eb32 Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 11 May 2026 20:03:37 +0400 Subject: [PATCH 02/69] feat: swap native bridge test --- src/dummyRouter.sol | 15 +- src/manipulators/MathManipulator.sol | 19 +++ test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 2 +- ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 147 ++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/manipulators/MathManipulator.sol create mode 100644 test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol diff --git a/src/dummyRouter.sol b/src/dummyRouter.sol index e0b5ef9..aab54cc 100644 --- a/src/dummyRouter.sol +++ b/src/dummyRouter.sol @@ -4,7 +4,8 @@ pragma solidity =0.8.25; contract DummyRouter { enum CallType { CALL, - STATICCALL + STATICCALL, + CALL_WITH_NATIVE } struct Splice { @@ -24,6 +25,7 @@ contract DummyRouter { error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); error CallFailed(uint256 actionIndex, bytes returndata); + error MissingNativeValue(uint256 actionIndex); function execute(Action[] calldata actions) external payable returns (bytes[] memory results) { results = new bytes[](actions.length); @@ -51,6 +53,15 @@ contract DummyRouter { if (actions[i].callType == CallType.STATICCALL) { (success, ret) = actions[i].target.staticcall(callData); + } else if (actions[i].callType == CallType.CALL_WITH_NATIVE) { + if (callData.length < 32) revert MissingNativeValue(i); + uint256 callValue; + bytes memory payload = new bytes(callData.length - 32); + assembly ("memory-safe") { + callValue := mload(add(callData, 0x20)) + mcopy(add(payload, 0x20), add(callData, 0x40), mload(payload)) + } + (success, ret) = actions[i].target.call{value: callValue}(payload); } else { (success, ret) = actions[i].target.call(callData); } @@ -60,4 +71,6 @@ contract DummyRouter { results[i] = ret; } } + + receive() external payable {} } diff --git a/src/manipulators/MathManipulator.sol b/src/manipulators/MathManipulator.sol new file mode 100644 index 0000000..257b800 --- /dev/null +++ b/src/manipulators/MathManipulator.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.25; + +/// @notice Generic arithmetic helpers for router calldata splicing. +contract MathManipulator { + uint256 internal constant BPS_DENOMINATOR = 10_000; + + function add(uint256 a, uint256 b) external pure returns (uint256) { + return a + b; + } + + function subtract(uint256 a, uint256 b) external pure returns (uint256) { + return a - b; + } + + function percent(uint256 amount, uint256 bps) external pure returns (uint256) { + return amount * bps / BPS_DENOMINATOR; + } +} diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 624f9a9..5ffd2fa 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -23,7 +23,7 @@ interface ISpokePool { bytes calldata message ) external payable; } - +// ref tx 0xc0ba134856d0151eebfeb67aabe0eb12db248974f4d78b9d358a6d46dcaa9700 contract OpenOceanAcrossOpenRouterPoCTest is Test { address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; address internal constant ACROSS_ARBITRUM_SPOKE_POOL = 0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A; diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol new file mode 100644 index 0000000..6688ccc --- /dev/null +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {DummyRouter} from "../../src/dummyRouter.sol"; +import {MathManipulator} from "../../src/manipulators/MathManipulator.sol"; + +// ref tx 0xef65dc3323cd757c5e3a1a872b99beff6e71f0a80b1a2a6d280d2f2458f3cbaf +contract OpenOceanStargateNativeOpenRouterPoCTest is Test { + address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant STARGATE_NATIVE_WRAPPER = 0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + + uint256 internal constant FORK_BLOCK_NUMBER = 461_745_499; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01f38b; + uint256 internal constant SWAP_INPUT_USDC = 0x1312d00; + uint256 internal constant STARGATE_NATIVE_FEE = 0x1603e90a5fe0; + + uint256 internal constant STARGATE_AMOUNT_OFFSET = 196; + uint256 internal constant STARGATE_MIN_AMOUNT_OFFSET = 228; + uint256 internal constant CALL_WITH_NATIVE_PAYLOAD_OFFSET = 32; + + string internal constant OPENOCEAN_SWAP_CALLDATA = + "0x0a9704d5000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a20000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000000000000000000000000000001b91a33e163bdf000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000092000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b9470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112a880000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab100000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af950800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af9508000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004453c059a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001649f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000400000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e85000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000000000000000000000000000000000000000000000000001ea1d1d335261500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + string internal constant STARGATE_NATIVE_CALLDATA = + "0xc7c7f5b3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000001603e90a5fe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b0bbff6311b7f245761a7846d3ce7b1b100c183600000000000000000000000000000000000000000000000000000000000075e8000000000000000000000000b0bbff6311b7f245761a7846d3ce7b1b100c1836000000000000000000000000000000000000000000000000001ea0ae9d021ce2000000000000000000000000000000000000000000000000001e51722595900000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + function test_openOceanSwapStargateNativeBridge_arbitrumFork() public { + string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("ARBITRUM_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + DummyRouter router = _routerAtFixtureAddress(); + MathManipulator manipulator = new MathManipulator(); + if (bytes(rpcUrl).length == 0) { + emit log("Set ARBITRUM_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_USDC_AMOUNT", SWAP_INPUT_USDC); + uint256 nativeFee = vm.envOr("POC_STARGATE_NATIVE_FEE", STARGATE_NATIVE_FEE); + + deal(ARBITRUM_USDC, address(router), inputAmount); + uint256 initialNativeBalance = address(router).balance; + uint256 initialWethBalance = ERC20(ARBITRUM_WETH).balanceOf(address(router)); + + bytes memory stargateCalldata = vm.parseBytes(STARGATE_NATIVE_CALLDATA); + _writeWord(stargateCalldata, STARGATE_AMOUNT_OFFSET, 0); + _writeWord(stargateCalldata, STARGATE_MIN_AMOUNT_OFFSET, 0); + + DummyRouter.Action[] memory actions = + _buildActions(manipulator, inputAmount, nativeFee, vm.parseBytes(OPENOCEAN_SWAP_CALLDATA), stargateCalldata); + + bytes[] memory results = router.execute(actions); + + _assertPocResult(router, nativeFee, initialNativeBalance, initialWethBalance, results[1], results[2]); + } + + function _buildActions( + MathManipulator manipulator, + uint256 inputAmount, + uint256 nativeFee, + bytes memory swapCalldata, + bytes memory stargateCalldata + ) internal pure returns (DummyRouter.Action[] memory actions) { + actions = new DummyRouter.Action[](4); + actions[0] = _action( + DummyRouter.CallType.CALL, + ARBITRUM_USDC, + abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), + new DummyRouter.Splice[](0) + ); + + actions[1] = + _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new DummyRouter.Splice[](0)); + + DummyRouter.Splice[] memory bridgeAmountSplices = new DummyRouter.Splice[](1); + bridgeAmountSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); + actions[2] = _action( + DummyRouter.CallType.STATICCALL, + address(manipulator), + abi.encodeCall(MathManipulator.subtract, (uint256(0), nativeFee)), + bridgeAmountSplices + ); + + DummyRouter.Splice[] memory stargateSplices = new DummyRouter.Splice[](2); + stargateSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 0, length: 32}); + stargateSplices[1] = DummyRouter.Splice({ + sourceActionIndex: 2, + srcOffset: 0, + dstOffset: CALL_WITH_NATIVE_PAYLOAD_OFFSET + STARGATE_AMOUNT_OFFSET, + length: 32 + }); + actions[3] = _action( + DummyRouter.CallType.CALL_WITH_NATIVE, + STARGATE_NATIVE_WRAPPER, + abi.encodePacked(uint256(0), stargateCalldata), + stargateSplices + ); + } + + function _assertPocResult( + DummyRouter router, + uint256 nativeFee, + uint256 initialNativeBalance, + uint256 initialWethBalance, + bytes memory openOceanResult, + bytes memory bridgeAmountResult + ) internal view { + uint256 swapOutput = abi.decode(openOceanResult, (uint256)); + uint256 bridgeAmount = abi.decode(bridgeAmountResult, (uint256)); + + assertGt(swapOutput, 0); + assertEq(bridgeAmount + nativeFee, swapOutput); + assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); + assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), initialWethBalance); + assertLt(address(router).balance - initialNativeBalance, nativeFee); + } + + function _routerAtFixtureAddress() internal returns (DummyRouter router) { + DummyRouter implementation = new DummyRouter(); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return DummyRouter(payable(FIXTURE_ROUTER)); + } + + function _action( + DummyRouter.CallType callType, + address target, + bytes memory data, + DummyRouter.Splice[] memory splices + ) internal pure returns (DummyRouter.Action memory) { + return DummyRouter.Action({callType: callType, target: target, data: data, splices: splices}); + } + + function _writeWord(bytes memory data, uint256 offset, uint256 value) internal pure { + assembly ("memory-safe") { + mstore(add(add(data, 0x20), offset), value) + } + } +} From 8ec91bd1eab537241f0295b340f61f6504fe5414 Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 11 May 2026 20:19:21 +0400 Subject: [PATCH 03/69] feat: construct openocean, stargate data and collect fee --- ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 209 ++++++++++++++++-- 1 file changed, 185 insertions(+), 24 deletions(-) diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index 6688ccc..2cef834 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -7,28 +7,72 @@ import {ERC20} from "solady/src/tokens/ERC20.sol"; import {DummyRouter} from "../../src/dummyRouter.sol"; import {MathManipulator} from "../../src/manipulators/MathManipulator.sol"; +interface IOpenOceanExchangeV2 { + struct SwapDescription { + address srcToken; + address dstToken; + address srcReceiver; + address dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + address referrer; + bytes permit; + } + + struct CallDescription { + uint256 target; + uint256 gasLimit; + uint256 value; + bytes data; + } +} + +interface IStargateNative { + struct SendParam { + uint32 dstEid; + bytes32 to; + uint256 amountLD; + uint256 minAmountLD; + bytes extraOptions; + bytes composeMsg; + bytes oftCmd; + } + + struct MessagingFee { + uint256 nativeFee; + uint256 lzTokenFee; + } + + function send(SendParam calldata sendParam, MessagingFee calldata fee, address refundAddress) external payable; +} + // ref tx 0xef65dc3323cd757c5e3a1a872b99beff6e71f0a80b1a2a6d280d2f2458f3cbaf contract OpenOceanStargateNativeOpenRouterPoCTest is Test { + bytes4 internal constant OPENOCEAN_SWAP_SELECTOR = 0x0a9704d5; address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant OPENOCEAN_CALLER = 0xB100a5B2591Dd099040a5ab76EFe682A6D8a48a2; + address internal constant OPENOCEAN_REFERRER = 0x38c7720238a2C123814aaF1A3D0e31E0093aF046; address internal constant STARGATE_NATIVE_WRAPPER = 0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F; address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant FEE_RECIPIENT = 0x0079a23EDEA601190EdF1cda05c8Af3fEA2f2d9F; address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + address internal constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 internal constant FORK_BLOCK_NUMBER = 461_745_499; uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01f38b; uint256 internal constant SWAP_INPUT_USDC = 0x1312d00; + uint256 internal constant OPENOCEAN_MIN_RETURN = 0x1b91a33e163bdf; + uint256 internal constant OPENOCEAN_FLAGS = 2; uint256 internal constant STARGATE_NATIVE_FEE = 0x1603e90a5fe0; + uint256 internal constant ROUTE_FEE_BPS = 100; + uint32 internal constant BASE_ENDPOINT_ID = 30_184; uint256 internal constant STARGATE_AMOUNT_OFFSET = 196; - uint256 internal constant STARGATE_MIN_AMOUNT_OFFSET = 228; uint256 internal constant CALL_WITH_NATIVE_PAYLOAD_OFFSET = 32; - string internal constant OPENOCEAN_SWAP_CALLDATA = - "0x0a9704d5000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a20000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000000000000000000000000000001b91a33e163bdf000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000092000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b9470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112a880000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab100000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af950800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af9508000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004453c059a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001649f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000400000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e85000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000000000000000000000000000000000000000000000000001ea1d1d335261500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - string internal constant STARGATE_NATIVE_CALLDATA = - "0xc7c7f5b3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000001603e90a5fe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b0bbff6311b7f245761a7846d3ce7b1b100c183600000000000000000000000000000000000000000000000000000000000075e8000000000000000000000000b0bbff6311b7f245761a7846d3ce7b1b100c1836000000000000000000000000000000000000000000000000001ea0ae9d021ce2000000000000000000000000000000000000000000000000001e51722595900000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - function test_openOceanSwapStargateNativeBridge_arbitrumFork() public { string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); if (bytes(rpcUrl).length != 0) { @@ -49,18 +93,104 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { deal(ARBITRUM_USDC, address(router), inputAmount); uint256 initialNativeBalance = address(router).balance; + uint256 initialFeeRecipientBalance = FEE_RECIPIENT.balance; uint256 initialWethBalance = ERC20(ARBITRUM_WETH).balanceOf(address(router)); - bytes memory stargateCalldata = vm.parseBytes(STARGATE_NATIVE_CALLDATA); - _writeWord(stargateCalldata, STARGATE_AMOUNT_OFFSET, 0); - _writeWord(stargateCalldata, STARGATE_MIN_AMOUNT_OFFSET, 0); - - DummyRouter.Action[] memory actions = - _buildActions(manipulator, inputAmount, nativeFee, vm.parseBytes(OPENOCEAN_SWAP_CALLDATA), stargateCalldata); + DummyRouter.Action[] memory actions = _buildActions( + manipulator, inputAmount, nativeFee, _openOceanSwapCalldata(inputAmount), _stargateCalldata(nativeFee) + ); bytes[] memory results = router.execute(actions); - _assertPocResult(router, nativeFee, initialNativeBalance, initialWethBalance, results[1], results[2]); + _assertPocResult( + router, + nativeFee, + initialNativeBalance, + initialFeeRecipientBalance, + initialWethBalance, + results[1], + results[2], + results[4], + results[5] + ); + } + + function _openOceanSwapCalldata(uint256 inputAmount) internal pure returns (bytes memory) { + return abi.encodeWithSelector( + OPENOCEAN_SWAP_SELECTOR, + OPENOCEAN_CALLER, + IOpenOceanExchangeV2.SwapDescription({ + srcToken: ARBITRUM_USDC, + dstToken: NATIVE_TOKEN, + srcReceiver: OPENOCEAN_CALLER, + dstReceiver: FIXTURE_ROUTER, + amount: inputAmount, + minReturnAmount: OPENOCEAN_MIN_RETURN, + flags: OPENOCEAN_FLAGS, + referrer: OPENOCEAN_REFERRER, + permit: "" + }), + _openOceanCalls() + ); + } + + function _stargateCalldata(uint256 nativeFee) internal pure returns (bytes memory) { + return abi.encodeCall( + IStargateNative.send, + ( + IStargateNative.SendParam({ + dstEid: BASE_ENDPOINT_ID, + to: _toBytes32(FIXTURE_RECIPIENT), + amountLD: 0, + minAmountLD: 0, + extraOptions: "", + composeMsg: "", + oftCmd: "" + }), + IStargateNative.MessagingFee({nativeFee: nativeFee, lzTokenFee: 0}), + FIXTURE_RECIPIENT + ) + ); + } + + function _openOceanCalls() internal pure returns (IOpenOceanExchangeV2.CallDescription[] memory calls) { + calls = new IOpenOceanExchangeV2.CallDescription[](6); + calls[0] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b9470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112a880000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab1000003000000000000000000000000000000000000" + }); + calls[1] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af9508000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + calls[2] = IOpenOceanExchangeV2.CallDescription({ + target: uint256(uint160(0xb7236B927e03542AC3bE0A054F2bEa8868AF9508)), + gasLimit: 0, + value: 0, + data: hex"53c059a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2" + }); + calls[3] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000400000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + calls[4] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"8a6a1e85000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000000000000000000000000000000000000000000000000001ea1d1d3352615" + }); + calls[5] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f865422000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); } function _buildActions( @@ -70,7 +200,7 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { bytes memory swapCalldata, bytes memory stargateCalldata ) internal pure returns (DummyRouter.Action[] memory actions) { - actions = new DummyRouter.Action[](4); + actions = new DummyRouter.Action[](7); actions[0] = _action( DummyRouter.CallType.CALL, ARBITRUM_USDC, @@ -81,9 +211,34 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { actions[1] = _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new DummyRouter.Splice[](0)); - DummyRouter.Splice[] memory bridgeAmountSplices = new DummyRouter.Splice[](1); - bridgeAmountSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); + DummyRouter.Splice[] memory feeSplices = new DummyRouter.Splice[](1); + feeSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); actions[2] = _action( + DummyRouter.CallType.STATICCALL, + address(manipulator), + abi.encodeCall(MathManipulator.percent, (uint256(0), ROUTE_FEE_BPS)), + feeSplices + ); + + DummyRouter.Splice[] memory feeTransferSplices = new DummyRouter.Splice[](1); + feeTransferSplices[0] = DummyRouter.Splice({sourceActionIndex: 2, srcOffset: 0, dstOffset: 0, length: 32}); + actions[3] = _action( + DummyRouter.CallType.CALL_WITH_NATIVE, FEE_RECIPIENT, abi.encodePacked(uint256(0)), feeTransferSplices + ); + + DummyRouter.Splice[] memory postFeeSplices = new DummyRouter.Splice[](2); + postFeeSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); + postFeeSplices[1] = DummyRouter.Splice({sourceActionIndex: 2, srcOffset: 0, dstOffset: 36, length: 32}); + actions[4] = _action( + DummyRouter.CallType.STATICCALL, + address(manipulator), + abi.encodeCall(MathManipulator.subtract, (uint256(0), uint256(0))), + postFeeSplices + ); + + DummyRouter.Splice[] memory bridgeAmountSplices = new DummyRouter.Splice[](1); + bridgeAmountSplices[0] = DummyRouter.Splice({sourceActionIndex: 4, srcOffset: 0, dstOffset: 4, length: 32}); + actions[5] = _action( DummyRouter.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.subtract, (uint256(0), nativeFee)), @@ -91,14 +246,14 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { ); DummyRouter.Splice[] memory stargateSplices = new DummyRouter.Splice[](2); - stargateSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 0, length: 32}); + stargateSplices[0] = DummyRouter.Splice({sourceActionIndex: 4, srcOffset: 0, dstOffset: 0, length: 32}); stargateSplices[1] = DummyRouter.Splice({ - sourceActionIndex: 2, + sourceActionIndex: 5, srcOffset: 0, dstOffset: CALL_WITH_NATIVE_PAYLOAD_OFFSET + STARGATE_AMOUNT_OFFSET, length: 32 }); - actions[3] = _action( + actions[6] = _action( DummyRouter.CallType.CALL_WITH_NATIVE, STARGATE_NATIVE_WRAPPER, abi.encodePacked(uint256(0), stargateCalldata), @@ -110,15 +265,23 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { DummyRouter router, uint256 nativeFee, uint256 initialNativeBalance, + uint256 initialFeeRecipientBalance, uint256 initialWethBalance, bytes memory openOceanResult, + bytes memory feeResult, + bytes memory postFeeResult, bytes memory bridgeAmountResult ) internal view { uint256 swapOutput = abi.decode(openOceanResult, (uint256)); + uint256 routeFee = abi.decode(feeResult, (uint256)); + uint256 postFeeAmount = abi.decode(postFeeResult, (uint256)); uint256 bridgeAmount = abi.decode(bridgeAmountResult, (uint256)); assertGt(swapOutput, 0); - assertEq(bridgeAmount + nativeFee, swapOutput); + assertEq(routeFee, swapOutput * ROUTE_FEE_BPS / 10_000); + assertEq(FEE_RECIPIENT.balance - initialFeeRecipientBalance, routeFee); + assertEq(postFeeAmount + routeFee, swapOutput); + assertEq(bridgeAmount + nativeFee, postFeeAmount); assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), initialWethBalance); assertLt(address(router).balance - initialNativeBalance, nativeFee); @@ -139,9 +302,7 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { return DummyRouter.Action({callType: callType, target: target, data: data, splices: splices}); } - function _writeWord(bytes memory data, uint256 offset, uint256 value) internal pure { - assembly ("memory-safe") { - mstore(add(add(data, 0x20), offset), value) - } + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); } } From faf878650eaf2ff5e87bd23a791116d2074bb343 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 00:29:08 +0530 Subject: [PATCH 04/69] feat: monolithic+modular combined router --- src/combined/BungeeOpenRouterV2.sol | 371 +++++++++++++++++++ src/combined/BungeeOpenRouterV2Unchecked.sol | 342 +++++++++++++++++ 2 files changed, 713 insertions(+) create mode 100644 src/combined/BungeeOpenRouterV2.sol create mode 100644 src/combined/BungeeOpenRouterV2Unchecked.sol diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol new file mode 100644 index 0000000..6f55732 --- /dev/null +++ b/src/combined/BungeeOpenRouterV2.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.25; + +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; + +import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; +import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; +import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; +import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; +import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; + +/// @title BungeeOpenRouterV2 +/// @notice Combined open-router that exposes two execution paths behind a +/// single signature-verified, AllowanceHolder-based fund pull: +/// +/// 1. `performExecution` — monolithic path. The signed payload describes +/// every step explicitly: pull, optional pre-swap fee, optional swap, +/// optional post-swap fee, bridge call with multi-position amount +/// splicing. Suitable for the vast majority of routes. +/// +/// 2. `performModularExecution` — generic action loop (identical to +/// `BungeeOpenRouterModular`). Each `Action` carries a list of +/// `Splice`s that copy byte ranges from the previous action's +/// returndata into this action's calldata before dispatch. Use this +/// for routes that need more than one bridge call, non-standard step +/// ordering, or multiple amount fields patched from a single prior +/// return value. +/// +/// Fund pulls always go through 0x AllowanceHolder (transient-storage +/// allowance). The `_msgSender() == user` guard ensures the AH +/// ephemeral allowance (keyed by operator + owner + token) belongs to +/// the user named in the signed payload. +/// +/// @dev Both entrypoints verify a personal_sign signature over +/// `keccak256(abi.encode(chainid, address(this), exec))` and consume a +/// single-use nonce, matching the `Solver` / `StakedRouterReceiver` +/// authentication model. +contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { + using SafeTransferLib for address; + + // ========================================================================= + // Monolithic execution types + // ========================================================================= + + /// @notice Who is sending funds and how much. + struct InputData { + address user; + address inputToken; + uint256 inputAmount; + } + + /// @notice Optional fee. Set `receiver` to address(0) and `amount` to 0 to skip. + struct FeeData { + address receiver; + uint256 amount; + } + + /// @notice Optional swap step. Set `target` to address(0) to skip entirely. + struct SwapData { + address target; + address approvalSpender; // 0 to skip ERC20 approval before swap + address outputToken; // token measured via balance delta + uint256 value; // ETH forwarded to the swap target + uint256 minOutput; // minimum balance delta; reverts if not met + bytes data; + } + + /// @notice Mandatory bridge call. `amountPositions` lists every byte offset + /// in `data` where the final post-fee amount must be written. + struct BridgeData { + address target; + address approvalSpender; // 0 to skip ERC20 approval before bridge + uint256 value; // ETH forwarded to the bridge target + bytes data; + uint256[] amountPositions; + } + + /// @notice Signed payload for the monolithic execution path. + /// @dev Digest: keccak256(abi.encode(block.chainid, address(this), exec)). + struct MonolithicExecution { + InputData input; + FeeData preFee; // taken in inputToken before swap + SwapData swap; + FeeData postFee; // taken in finalToken after swap + BridgeData bridge; + uint256 nonce; + uint256 deadline; + } + + // ========================================================================= + // Modular execution types + // ========================================================================= + + enum CallType { + CALL, + DELEGATECALL, + STATICCALL + } + + /// @notice Byte-range copy from the previous action's returndata into this + /// action's calldata, applied before the action is dispatched. + struct Splice { + uint256 srcOffset; // offset within the previous action's returndata + uint256 dstOffset; // offset within this action's `data` + uint256 length; // number of bytes to copy + } + + /// @notice One step in the modular execution pipeline. + struct Action { + CallType callType; + address target; + uint256 value; // ETH forwarded; must be zero for DELEGATECALL / STATICCALL + bytes data; // base calldata, patched in-place by splices before dispatch + Splice[] splices; // applied BEFORE this action runs + } + + /// @notice Signed payload for the modular execution path. + struct ModularExecution { + Action[] actions; + uint256 nonce; + uint256 deadline; + } + + // ========================================================================= + // Errors + // ========================================================================= + + error SwapOutputInsufficient(); + error InsufficientFunds(); + error InvalidExecution(); + error CallerNotSignedUser(); + error ValueOnNonCall(); + error EmptyActions(); + error UnknownCallType(); + + // ========================================================================= + // Constructor + // ========================================================================= + + constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} + + receive() external payable {} + + // ========================================================================= + // External: monolithic path + // ========================================================================= + + /** + * @notice Executes a monolithic signed payload: pull funds via AH, optional + * pre-swap fee, optional swap, optional post-swap fee, bridge call + * with multi-position amount splicing. + * @dev Anyone may call; security is the backend signature + single-use nonce. + * The caller MUST route through `AllowanceHolder.exec` so that + * `_msgSender()` resolves to `exec.input.user`. + */ + function performExecution(MonolithicExecution calldata exec, bytes calldata signature) external payable { + bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); + _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); + _runMonolithic(exec); + } + + // ========================================================================= + // External: modular path + // ========================================================================= + + /** + * @notice Executes a signed sequence of generic actions with optional + * returndata splicing between steps. + * @dev The signed digest covers the entire action set, so the caller cannot + * reorder, retarget, or strip splices from any action. + */ + function performModularExecution(ModularExecution calldata exec, bytes calldata signature) external payable { + bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); + _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); + _performActions(exec.actions); + } + + // ========================================================================= + // Internal: monolithic pipeline + // ========================================================================= + + function _runMonolithic(MonolithicExecution calldata exec) internal { + if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { + revert InvalidExecution(); + } + + // 1. pull funds from user via AllowanceHolder + _pullFromUser(exec.input.inputToken, exec.input.user, exec.input.inputAmount); + + // 2. optional pre-swap fee in input token + if (exec.preFee.amount != 0) { + CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); + } + + // 3. optional swap, accounted via pre/post balance delta + address finalToken; + uint256 finalAmount; + if (exec.swap.target != address(0)) { + (finalToken, finalAmount) = _performSwap(exec); + } else { + if (exec.preFee.amount > exec.input.inputAmount) { + revert InsufficientFunds(); + } + finalToken = exec.input.inputToken; + unchecked { + finalAmount = exec.input.inputAmount - exec.preFee.amount; + } + } + + // 4. optional post-swap fee in final token + if (exec.postFee.amount != 0) { + if (exec.postFee.amount > finalAmount) { + revert InsufficientFunds(); + } + CurrencyLib.transfer(finalToken, exec.postFee.receiver, exec.postFee.amount); + unchecked { + finalAmount -= exec.postFee.amount; + } + } + + // 5. splice finalAmount into bridge calldata at every signed offset + bytes memory bridgeData = exec.bridge.data; + BytesSpliceLib.spliceWords({data: bridgeData, positions: exec.bridge.amountPositions, word: finalAmount}); + + // 6. optional approval to bridge spender + if (exec.bridge.approvalSpender != address(0) && finalToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(finalToken, exec.bridge.approvalSpender, finalAmount); + } + + // 7. bridge call, bubbling any revert + _performAction(exec.bridge.target, exec.bridge.value, bridgeData); + } + + /// @dev Balance-delta swap helper; split out to keep _runMonolithic under 100 lines. + function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { + uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); + + if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + uint256 swapInput; + unchecked { + swapInput = exec.input.inputAmount - exec.preFee.amount; + } + SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); + } + + _performAction(exec.swap.target, exec.swap.value, exec.swap.data); + + uint256 postBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); + if (postBalance < preBalance) { + revert SwapOutputInsufficient(); + } + uint256 delta; + unchecked { + delta = postBalance - preBalance; + } + if (delta < exec.swap.minOutput) { + revert SwapOutputInsufficient(); + } + + finalToken = exec.swap.outputToken; + finalAmount = delta; + } + + // ========================================================================= + // Internal: AllowanceHolder pull + // ========================================================================= + + /** + * @notice Pulls `amount` of `token` from `user` via AllowanceHolder. + * @dev Requires the caller to have routed through `AllowanceHolder.exec` + * so `_msgSender()` resolves to the original user. Mirrors the + * assembly in `0x-settler/src/core/Permit2Payment.sol`. + * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea + */ + function _pullFromUser(address token, address user, uint256 amount) internal { + if (_msgSender() != user) { + revert CallerNotSignedUser(); + } + + address allowanceHolder = address(ALLOWANCE_HOLDER); + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(add(0x80, ptr), amount) + mstore(add(0x60, ptr), address()) + mstore(add(0x4c, ptr), shl(0x60, user)) // clears recipient padding + mstore(add(0x2c, ptr), shl(0xa0, token)) // clears owner padding + mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding + + if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { + let p := mload(0x40) + returndatacopy(p, 0x00, returndatasize()) + revert(p, returndatasize()) + } + } + } + + // ========================================================================= + // Internal: modular action loop + // ========================================================================= + + /** + * @notice Runs a signed sequence of actions, applying returndata splices + * from each step into the calldata of the next before dispatch. + */ + function _performActions(Action[] calldata actions) internal { + if (actions.length == 0) { + revert EmptyActions(); + } + + bytes memory prevReturn; // empty on first action; splice on action[0] is illegal + for (uint256 i = 0; i < actions.length;) { + Action calldata a = actions[i]; + bytes memory data = a.data; + + // apply splices: copy byte ranges from prevReturn into this action's data + uint256 spLen = a.splices.length; + for (uint256 j = 0; j < spLen;) { + Splice calldata sp = a.splices[j]; + BytesSpliceLib.spliceBytes({ + dst: data, // this action's calldata (base is signed; patched before dispatch) + dstOffset: sp.dstOffset, // write `length` bytes into `dst` starting here + src: prevReturn, // read from the previous action's returndata + srcOffset: sp.srcOffset, // copy slice starting at this offset in `src` + length: sp.length // number of bytes to copy (overwrites same span in `dst`) + }); + unchecked { + ++j; + } + } + + prevReturn = _dispatchAction(a.callType, a.target, a.value, data); + unchecked { + ++i; + } + } + } + + /** + * @notice Dispatches a single action with the given call type; bubbles revert. + * @dev Named `_dispatchAction` (rather than overloading `_performAction`) + * to keep the CALL-only base helper in `OpenRouterAuthBase` distinct + * from this three-way dispatcher. + */ + function _dispatchAction(CallType callType, address target, uint256 value, bytes memory data) + internal + returns (bytes memory ret) + { + bool ok; + if (callType == CallType.CALL) { + (ok, ret) = target.call{value: value}(data); + } else if (callType == CallType.DELEGATECALL) { + if (value != 0) { + revert ValueOnNonCall(); + } + (ok, ret) = target.delegatecall(data); + } else if (callType == CallType.STATICCALL) { + if (value != 0) { + revert ValueOnNonCall(); + } + (ok, ret) = target.staticcall(data); + } else { + revert UnknownCallType(); + } + + if (!ok) { + assembly ("memory-safe") { + revert(add(ret, 0x20), mload(ret)) + } + } + } +} diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol new file mode 100644 index 0000000..ea99d59 --- /dev/null +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.25; + +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; + +import {Ownable} from "../common/utils/Ownable.sol"; +import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; +import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; +import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; +import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; + +/// @title BungeeOpenRouterV2Unchecked +/// @notice Identical execution logic to `BungeeOpenRouterV2` with all backend +/// signature verification removed. There are no nonce or deadline +/// fields; either entrypoint can be called by anyone. +/// +/// Fund safety still rests on AllowanceHolder's transient allowance +/// scoping (operator + owner + token): only the user whose address was +/// passed to `AllowanceHolder.exec` can authorise a pull of their own +/// funds. The `_msgSender() == user` check in `_pullFromUser` enforces +/// this at the contract level. +/// +/// Intended for development / testing environments where spinning up a +/// backend signer is inconvenient, or for operational flows where the +/// operator calls through AllowanceHolder directly without a separate +/// signing step. Do NOT deploy to production without adding an access +/// control layer appropriate to your threat model. +/// +/// @dev Both struct types mirror their `BungeeOpenRouterV2` counterparts but +/// drop the `nonce` and `deadline` fields, which are only relevant for +/// signature-based replay protection. +contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { + using SafeTransferLib for address; + + // ========================================================================= + // Monolithic execution types + // ========================================================================= + + struct InputData { + address user; + address inputToken; + uint256 inputAmount; + } + + struct FeeData { + address receiver; + uint256 amount; + } + + struct SwapData { + address target; + address approvalSpender; + address outputToken; + uint256 value; + uint256 minOutput; + bytes data; + } + + struct BridgeData { + address target; + address approvalSpender; + uint256 value; + bytes data; + uint256[] amountPositions; + } + + struct MonolithicExecution { + InputData input; + FeeData preFee; + SwapData swap; + FeeData postFee; + BridgeData bridge; + } + + // ========================================================================= + // Modular execution types + // ========================================================================= + + enum CallType { + CALL, + DELEGATECALL, + STATICCALL + } + + struct Splice { + uint256 srcOffset; + uint256 dstOffset; + uint256 length; + } + + struct Action { + CallType callType; + address target; + uint256 value; + bytes data; + Splice[] splices; + } + + // ========================================================================= + // Errors + // ========================================================================= + + error SwapOutputInsufficient(); + error InsufficientFunds(); + error InvalidExecution(); + error CallerNotSignedUser(); + error ValueOnNonCall(); + error EmptyActions(); + error UnknownCallType(); + + // ========================================================================= + // Constructor + // ========================================================================= + + constructor(address _owner) Ownable(_owner) {} + + receive() external payable {} + + // ========================================================================= + // External: monolithic path + // ========================================================================= + + /** + * @notice Executes the monolithic pipeline without signature verification: + * pull via AH, optional pre-swap fee, optional swap, optional + * post-swap fee, bridge call with multi-position amount splicing. + * @dev The caller MUST route through `AllowanceHolder.exec` so that + * `_msgSender()` resolves to `exec.input.user`. There is no nonce or + * deadline; replay protection is the caller's responsibility. + */ + function performExecution(MonolithicExecution calldata exec) external payable { + _runMonolithic(exec); + } + + // ========================================================================= + // External: modular path + // ========================================================================= + + /** + * @notice Runs a sequence of generic actions with optional returndata + * splicing between steps. No signature verification. + */ + function performModularExecution(Action[] calldata actions) external payable { + _performActions(actions); + } + + // ========================================================================= + // Internal: monolithic pipeline + // ========================================================================= + + function _runMonolithic(MonolithicExecution calldata exec) internal { + if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { + revert InvalidExecution(); + } + + // 1. pull funds from user via AllowanceHolder + _pullFromUser(exec.input.inputToken, exec.input.user, exec.input.inputAmount); + + // 2. optional pre-swap fee in input token + if (exec.preFee.amount != 0) { + CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); + } + + // 3. optional swap, accounted via pre/post balance delta + address finalToken; + uint256 finalAmount; + if (exec.swap.target != address(0)) { + (finalToken, finalAmount) = _performSwap(exec); + } else { + if (exec.preFee.amount > exec.input.inputAmount) { + revert InsufficientFunds(); + } + finalToken = exec.input.inputToken; + unchecked { + finalAmount = exec.input.inputAmount - exec.preFee.amount; + } + } + + // 4. optional post-swap fee in final token + if (exec.postFee.amount != 0) { + if (exec.postFee.amount > finalAmount) { + revert InsufficientFunds(); + } + CurrencyLib.transfer(finalToken, exec.postFee.receiver, exec.postFee.amount); + unchecked { + finalAmount -= exec.postFee.amount; + } + } + + // 5. splice finalAmount into bridge calldata at every signed offset + bytes memory bridgeData = exec.bridge.data; + BytesSpliceLib.spliceWords({data: bridgeData, positions: exec.bridge.amountPositions, word: finalAmount}); + + // 6. optional approval to bridge spender + if (exec.bridge.approvalSpender != address(0) && finalToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(finalToken, exec.bridge.approvalSpender, finalAmount); + } + + // 7. bridge call, bubbling any revert + _doCall(exec.bridge.target, exec.bridge.value, bridgeData); + } + + /// @dev Balance-delta swap helper. + function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { + uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); + + if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + uint256 swapInput; + unchecked { + swapInput = exec.input.inputAmount - exec.preFee.amount; + } + SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); + } + + _doCall(exec.swap.target, exec.swap.value, exec.swap.data); + + uint256 postBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); + if (postBalance < preBalance) { + revert SwapOutputInsufficient(); + } + uint256 delta; + unchecked { + delta = postBalance - preBalance; + } + if (delta < exec.swap.minOutput) { + revert SwapOutputInsufficient(); + } + + finalToken = exec.swap.outputToken; + finalAmount = delta; + } + + // ========================================================================= + // Internal: AllowanceHolder pull + // ========================================================================= + + /** + * @notice Pulls `amount` of `token` from `user` via AllowanceHolder. + * @dev Enforces `_msgSender() == user`: the caller must have routed through + * `AllowanceHolder.exec` whose `owner` argument matches `user`. + * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea + */ + function _pullFromUser(address token, address user, uint256 amount) internal { + if (_msgSender() != user) { + revert CallerNotSignedUser(); + } + + address allowanceHolder = address(ALLOWANCE_HOLDER); + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(add(0x80, ptr), amount) + mstore(add(0x60, ptr), address()) + mstore(add(0x4c, ptr), shl(0x60, user)) // clears recipient padding + mstore(add(0x2c, ptr), shl(0xa0, token)) // clears owner padding + mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding + + if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { + let p := mload(0x40) + returndatacopy(p, 0x00, returndatasize()) + revert(p, returndatasize()) + } + } + } + + // ========================================================================= + // Internal: modular action loop + // ========================================================================= + + function _performActions(Action[] calldata actions) internal { + if (actions.length == 0) { + revert EmptyActions(); + } + + bytes memory prevReturn; + for (uint256 i = 0; i < actions.length;) { + Action calldata a = actions[i]; + bytes memory data = a.data; + + uint256 spLen = a.splices.length; + for (uint256 j = 0; j < spLen;) { + Splice calldata sp = a.splices[j]; + BytesSpliceLib.spliceBytes({ + dst: data, // this action's calldata (patched before dispatch) + dstOffset: sp.dstOffset, // write `length` bytes into `dst` starting here + src: prevReturn, // read from the previous action's returndata + srcOffset: sp.srcOffset, // copy slice starting at this offset in `src` + length: sp.length // number of bytes to copy (overwrites same span in `dst`) + }); + unchecked { + ++j; + } + } + + prevReturn = _dispatchAction(a.callType, a.target, a.value, data); + unchecked { + ++i; + } + } + } + + function _dispatchAction(CallType callType, address target, uint256 value, bytes memory data) + internal + returns (bytes memory ret) + { + bool ok; + if (callType == CallType.CALL) { + (ok, ret) = target.call{value: value}(data); + } else if (callType == CallType.DELEGATECALL) { + if (value != 0) { + revert ValueOnNonCall(); + } + (ok, ret) = target.delegatecall(data); + } else if (callType == CallType.STATICCALL) { + if (value != 0) { + revert ValueOnNonCall(); + } + (ok, ret) = target.staticcall(data); + } else { + revert UnknownCallType(); + } + + if (!ok) { + assembly ("memory-safe") { + revert(add(ret, 0x20), mload(ret)) + } + } + } + + // ========================================================================= + // Internal: simple call dispatcher (used by monolithic path) + // ========================================================================= + + function _doCall(address target, uint256 value, bytes memory data) internal returns (bytes memory ret) { + bool ok; + (ok, ret) = target.call{value: value}(data); + if (!ok) { + assembly ("memory-safe") { + revert(add(ret, 0x20), mload(ret)) + } + } + } +} From 3d7f8b4bd5e1752216d2c355a09bf956848dad09 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 00:29:16 +0530 Subject: [PATCH 05/69] feat: hardhat, deploy script --- .env.example | 48 + .gitignore | 3 + OPENROUTER.md | 17 + hardhat.config.ts | 287 + package-lock.json | 8124 ++++++++++++++++++++ package.json | 18 + remappings.txt | 2 + scripts/deploy/deployBungeeOpenRouterV2.ts | 79 + tsconfig.json | 12 + 9 files changed, 8590 insertions(+) create mode 100644 .env.example create mode 100644 hardhat.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 remappings.txt create mode 100644 scripts/deploy/deployBungeeOpenRouterV2.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..35e585b --- /dev/null +++ b/.env.example @@ -0,0 +1,48 @@ +# Private key of the deployer wallet (no 0x prefix) +DEPLOYER_PRIVATE_KEY= + +# Constructor arguments +OWNER_ADDRESS= +OPEN_ROUTER_SIGNER_ADDRESS= # only needed for BungeeOpenRouterV2 (not Unchecked) + +# RPC endpoints (public fallbacks are pre-configured in hardhat.config.ts) +ETHEREUM_RPC= +POLYGON_RPC= +ARBITRUM_RPC= +OPTIMISM_RPC= +BASE_RPC= +AVALANCHE_RPC= +BSC_RPC= +LINEA_RPC= +SCROLL_RPC= +BLAST_RPC= +MODE_RPC= +MANTLE_RPC= +GNOSIS_RPC= +SONIC_RPC= +UNICHAIN_RPC= +BERACHAIN_RPC= +INK_RPC= +SONEIUM_RPC= +WORLDCHAIN_RPC= +SEI_RPC= +ARBITRUM_SEPOLIA_RPC= +OPTIMISM_SEPOLIA_RPC= + +# Block explorer API keys (for --verify) +MAINNET_ETHERSCAN_KEY= +POLYGON_ETHERSCAN_KEY= +ARBITRUM_ETHERSCAN_KEY= +OPTIMISM_ETHERSCAN_KEY= +BASE_ETHERSCAN_KEY= +BSC_ETHERSCAN_KEY= +AVALANCHE_ETHERSCAN_KEY= +LINEA_ETHERSCAN_KEY= +SCROLL_ETHERSCAN_KEY= +BLAST_ETHERSCAN_KEY= +MANTLE_ETHERSCAN_KEY= +GNOSIS_ETHERSCAN_KEY= +SONIC_ETHERSCAN_KEY= +UNICHAIN_ETHERSCAN_KEY= +BERACHAIN_ETHERSCAN_KEY= +SEI_ETHERSCAN_KEY= diff --git a/.gitignore b/.gitignore index 85198aa..1c14cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/ # Dotenv file .env + + +node_modules \ No newline at end of file diff --git a/OPENROUTER.md b/OPENROUTER.md index 6ab67b1..ced6eec 100644 --- a/OPENROUTER.md +++ b/OPENROUTER.md @@ -253,3 +253,20 @@ All live under `src/common/`. **`lib/BytesSpliceLib.sol`** — used by v1 (writing `finalAmount` to multiple positions in bridge calldata) and v2 (the per-splice `mcopy`). Exposes `spliceWord` (32-byte in-place overwrite, same assembly as `GenericStakedRoute`), `spliceWords` (repeat for multiple positions), and `spliceBytes` (arbitrary-length copy via `mcopy`, bounds-checked). **`allowance/AllowanceHolderContext.sol`**, **`interfaces/IAllowanceHolder.sol`** — imported only by the `*AH` contracts in each variant folder. + + + + +0. AllowanceHolder +1. OpenRouter -> Fee Transfer -> +2. OpenRouter (modify input) -> Swap execution -> OpenRouter (modify input) -> +3. AcrossManipulator -> OpenRouter (modify input) -> +4. SpokePool + +0. AllowanceHolder +1. OpenRouter -> Fee Transfer -> +2. OpenRouter (modify input) -> Swap execution -> OpenRouter (modify input) -> +3. AcrossRouter(amount, AcrossBridgeData) -> (modify SpokePool input with output ) -> SpokePool + +0. AllowanceHolder +1. AcrossRouter - should have all the fee, swap, bridge code in this \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 0000000..3b2bd20 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,287 @@ +import "@nomicfoundation/hardhat-foundry"; +import "@nomicfoundation/hardhat-toolbox"; +import { config as dotenvConfig } from "dotenv"; +import { HardhatUserConfig } from "hardhat/config"; +import { resolve } from "path"; + +dotenvConfig({ path: resolve(__dirname, "./.env") }); + +const deployerKey = process.env.DEPLOYER_PRIVATE_KEY; +const accounts = deployerKey ? [deployerKey] : []; + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.25", + settings: { + optimizer: { + enabled: true, + runs: 2000, + }, + evmVersion: "cancun", + }, + }, + networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, + ethereum: { + url: process.env.ETHEREUM_RPC ?? "https://eth.llamarpc.com", + chainId: 1, + accounts, + }, + polygon: { + url: process.env.POLYGON_RPC ?? "https://polygon.llamarpc.com", + chainId: 137, + accounts, + }, + arbitrum: { + url: process.env.ARBITRUM_RPC ?? "https://rpc.ankr.com/arbitrum", + chainId: 42161, + accounts, + }, + optimism: { + url: process.env.OPTIMISM_RPC ?? "https://mainnet.optimism.io", + chainId: 10, + accounts, + }, + base: { + url: process.env.BASE_RPC ?? "https://mainnet.base.org", + chainId: 8453, + accounts, + }, + avalanche: { + url: process.env.AVALANCHE_RPC ?? "https://rpc.ankr.com/avalanche", + chainId: 43114, + accounts, + }, + bsc: { + url: process.env.BSC_RPC ?? "https://bsc-dataseed.binance.org/", + chainId: 56, + accounts, + }, + linea: { + url: process.env.LINEA_RPC ?? "https://rpc.linea.build", + chainId: 59144, + accounts, + }, + scroll: { + url: process.env.SCROLL_RPC ?? "https://1rpc.io/scroll", + chainId: 534352, + accounts, + }, + blast: { + url: process.env.BLAST_RPC ?? "https://blastl2-mainnet.public.blastapi.io", + chainId: 81457, + accounts, + }, + mode: { + url: process.env.MODE_RPC ?? "https://1rpc.io/mode", + chainId: 34443, + accounts, + }, + mantle: { + url: process.env.MANTLE_RPC ?? "https://rpc.mantle.xyz", + chainId: 5000, + accounts, + }, + gnosis: { + url: process.env.GNOSIS_RPC ?? "https://rpc.ankr.com/gnosis", + chainId: 100, + accounts, + }, + sonic: { + url: process.env.SONIC_RPC ?? "https://rpc.ankr.com/sonic_mainnet", + chainId: 146, + accounts, + }, + unichain: { + url: process.env.UNICHAIN_RPC ?? "https://0xrpc.io/uni", + chainId: 130, + accounts, + }, + berachain: { + url: process.env.BERACHAIN_RPC ?? "https://berachain-rpc.publicnode.com", + chainId: 80094, + accounts, + }, + ink: { + url: process.env.INK_RPC ?? "https://rpc-gel.inkonchain.com", + chainId: 57073, + accounts, + }, + soneium: { + url: process.env.SONEIUM_RPC ?? "https://soneium.drpc.org", + chainId: 1868, + accounts, + }, + worldchain: { + url: process.env.WORLDCHAIN_RPC ?? "https://worldchain-mainnet.g.alchemy.com/public", + chainId: 480, + accounts, + }, + sei: { + url: process.env.SEI_RPC ?? "https://evm-rpc.sei-apis.com", + chainId: 1329, + accounts, + }, + // testnets + arbitrumSepolia: { + url: process.env.ARBITRUM_SEPOLIA_RPC ?? "https://arbitrum-sepolia-rpc.publicnode.com", + chainId: 421614, + accounts, + }, + optimismSepolia: { + url: process.env.OPTIMISM_SEPOLIA_RPC ?? "https://sepolia.optimism.io", + chainId: 11155420, + accounts, + }, + }, + etherscan: { + enabled: true, + apiKey: { + mainnet: process.env.MAINNET_ETHERSCAN_KEY ?? "", + ethereum: process.env.MAINNET_ETHERSCAN_KEY ?? "", + polygon: process.env.POLYGON_ETHERSCAN_KEY ?? "", + arbitrumOne: process.env.ARBITRUM_ETHERSCAN_KEY ?? "", + optimism: process.env.OPTIMISM_ETHERSCAN_KEY ?? "", + base: process.env.BASE_ETHERSCAN_KEY ?? "", + bsc: process.env.BSC_ETHERSCAN_KEY ?? "", + avalanche: process.env.AVALANCHE_ETHERSCAN_KEY ?? "", + linea: process.env.LINEA_ETHERSCAN_KEY ?? "", + scroll: process.env.SCROLL_ETHERSCAN_KEY ?? "", + blast: process.env.BLAST_ETHERSCAN_KEY ?? "", + mantle: process.env.MANTLE_ETHERSCAN_KEY ?? "", + gnosis: process.env.GNOSIS_ETHERSCAN_KEY ?? "", + sonic: process.env.SONIC_ETHERSCAN_KEY ?? "", + unichain: process.env.UNICHAIN_ETHERSCAN_KEY ?? "", + berachain: process.env.BERACHAIN_ETHERSCAN_KEY ?? "", + sei: process.env.SEI_ETHERSCAN_KEY ?? "", + arbitrumSepolia: process.env.ARBITRUM_ETHERSCAN_KEY ?? "", + optimismSepolia: process.env.OPTIMISM_ETHERSCAN_KEY ?? "", + }, + customChains: [ + { + network: "ethereum", + chainId: 1, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=1", browserURL: "https://etherscan.io" }, + }, + { + network: "optimism", + chainId: 10, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=10", browserURL: "https://optimistic.etherscan.io" }, + }, + { + network: "bsc", + chainId: 56, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=56", browserURL: "https://bscscan.com" }, + }, + { + network: "polygon", + chainId: 137, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=137", browserURL: "https://polygonscan.com" }, + }, + { + network: "mantle", + chainId: 5000, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=5000", browserURL: "https://mantlescan.xyz" }, + }, + { + network: "arbitrumOne", + chainId: 42161, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=42161", browserURL: "https://arbiscan.io" }, + }, + { + network: "avalanche", + chainId: 43114, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=43114", browserURL: "https://snowscan.xyz" }, + }, + { + network: "linea", + chainId: 59144, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=59144", browserURL: "https://lineascan.build" }, + }, + { + network: "base", + chainId: 8453, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=8453", browserURL: "https://basescan.org" }, + }, + { + network: "gnosis", + chainId: 100, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=100", browserURL: "https://gnosisscan.io" }, + }, + { + network: "blast", + chainId: 81457, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=81457", browserURL: "https://blastscan.io" }, + }, + { + network: "scroll", + chainId: 534352, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=534352", browserURL: "https://scrollscan.com" }, + }, + { + network: "mode", + chainId: 34443, + urls: { apiURL: "https://explorer.mode.network/api", browserURL: "https://explorer.mode.network" }, + }, + { + network: "sonic", + chainId: 146, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=146", browserURL: "https://sonicscan.org" }, + }, + { + network: "unichain", + chainId: 130, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=130", browserURL: "https://uniscan.xyz" }, + }, + { + network: "berachain", + chainId: 80094, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=80094", browserURL: "https://berascan.com" }, + }, + { + network: "ink", + chainId: 57073, + urls: { apiURL: "https://explorer.inkonchain.com/api", browserURL: "https://explorer.inkonchain.com" }, + }, + { + network: "soneium", + chainId: 1868, + urls: { apiURL: "https://soneium.blockscout.com/api", browserURL: "https://soneium.blockscout.com" }, + }, + { + network: "worldchain", + chainId: 480, + urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=480", browserURL: "https://worldscan.org" }, + }, + { + network: "sei", + chainId: 1329, + urls: { apiURL: "https://seitrace.com/pacific-1/api", browserURL: "https://seitrace.com" }, + }, + { + network: "arbitrumSepolia", + chainId: 421614, + urls: { apiURL: "https://api-sepolia.arbiscan.io/api", browserURL: "https://sepolia.arbiscan.io" }, + }, + { + network: "optimismSepolia", + chainId: 11155420, + urls: { apiURL: "https://api-sepolia-optimistic.etherscan.io/api", browserURL: "https://sepolia-optimism.etherscan.io" }, + }, + ], + }, + typechain: { + outDir: "typechain", + alwaysGenerateOverloads: true, + }, + paths: { + sources: "./src", + scripts: "./scripts", + cache: "./cache-hh", + artifacts: "./artifacts", + }, +}; + +export default config; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f3fbd15 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8124 @@ +{ + "name": "poc-openrouter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "poc-openrouter", + "version": "1.0.0", + "devDependencies": { + "@nomicfoundation/hardhat-foundry": "^1.1.2", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "dotenv": "^16.0.0", + "hardhat": "^2.22.7", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz", + "integrity": "sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-9.1.0.tgz", + "integrity": "sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^5.0.2", + "ethereum-cryptography": "^2.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nomicfoundation/edr": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.12.0-next.23.tgz", + "integrity": "sha512-F2/6HZh8Q9RsgkOIkRrckldbhPjIZY7d4mT9LYuW68miwGQ5l7CkAgcz9fRRiurA0+YJhtsbx/EyrD9DmX9BOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/edr-darwin-arm64": "0.12.0-next.23", + "@nomicfoundation/edr-darwin-x64": "0.12.0-next.23", + "@nomicfoundation/edr-linux-arm64-gnu": "0.12.0-next.23", + "@nomicfoundation/edr-linux-arm64-musl": "0.12.0-next.23", + "@nomicfoundation/edr-linux-x64-gnu": "0.12.0-next.23", + "@nomicfoundation/edr-linux-x64-musl": "0.12.0-next.23", + "@nomicfoundation/edr-win32-x64-msvc": "0.12.0-next.23" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-darwin-arm64": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.23.tgz", + "integrity": "sha512-Amh7mRoDzZyJJ4efqoePqdoZOzharmSOttZuJDlVE5yy07BoE8hL6ZRpa5fNYn0LCqn/KoWs8OHANWxhKDGhvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-darwin-x64": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.23.tgz", + "integrity": "sha512-9wn489FIQm7m0UCD+HhktjWx6vskZzeZD9oDc2k9ZvbBzdXwPp5tiDqUBJ+eQpByAzCDfteAJwRn2lQCE0U+Iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.23.tgz", + "integrity": "sha512-nlk5EejSzEUfEngv0Jkhqq3/wINIfF2ED9wAofc22w/V1DV99ASh9l3/e/MIHOQFecIZ9MDqt0Em9/oDyB1Uew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-musl": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.23.tgz", + "integrity": "sha512-SJuPBp3Rc6vM92UtVTUxZQ/QlLhLfwTftt2XUiYohmGKB3RjGzpgduEFMCA0LEnucUckU6UHrJNFHiDm77C4PQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-gnu": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.23.tgz", + "integrity": "sha512-NU+Qs3u7Qt6t3bJFdmmjd5CsvgI2bPPzO31KifM2Ez96/jsXYho5debtTQnimlb5NAqiHTSlxjh/F8ROcptmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-musl": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.23.tgz", + "integrity": "sha512-F78fZA2h6/ssiCSZOovlgIu0dUeI7ItKPsDDF3UUlIibef052GCXmliMinC90jVPbrjUADMd1BUwjfI0Z8OllQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-win32-x64-msvc": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.23.tgz", + "integrity": "sha512-IfJZQJn7d/YyqhmguBIGoCKjE9dKjbu6V6iNEPApfwf5JyyjHYyyfkLU4rf7hygj57bfH4sl1jtQ6r8HnT62lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/hardhat-chai-matchers": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.1.2.tgz", + "integrity": "sha512-NlUlde/ycXw2bLzA2gWjjbxQaD9xIRbAF30nsoEprAWzH8dXEI1ILZUKZMyux9n9iygEXTzN0SDVjE6zWDZi9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai-as-promised": "^7.1.3", + "chai-as-promised": "^7.1.1", + "deep-eql": "^4.0.1", + "ordinal": "^1.0.3" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "chai": "^4.2.0", + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-ethers": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.3.tgz", + "integrity": "sha512-208JcDeVIl+7Wu3MhFUUtiA8TJ7r2Rn3Wr+lSx9PfsDTKkbsAsWPY6N6wQ4mtzDv0/pB9nIbJhkjoHe1EsgNsA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "lodash.isequal": "^4.5.0" + }, + "peerDependencies": { + "ethers": "^6.14.0", + "hardhat": "^2.28.0" + } + }, + "node_modules/@nomicfoundation/hardhat-foundry": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.2.1.tgz", + "integrity": "sha512-pH1KeyI0sysgi7I7uQKPLXWl895EkuS6V41rSi820Ipqp/FScIwDh27RbevgC9zJ4ufSsSz34njm9cvRMGMNVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-ignition": { + "version": "0.15.16", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition/-/hardhat-ignition-0.15.16.tgz", + "integrity": "sha512-T0JTnuib7QcpsWkHCPLT7Z6F483EjTdcdjb1e00jqS9zTGCPqinPB66LLtR/duDLdvgoiCVS6K8WxTQkA/xR1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nomicfoundation/ignition-core": "^0.15.15", + "@nomicfoundation/ignition-ui": "^0.15.13", + "chalk": "^4.0.0", + "debug": "^4.3.2", + "fs-extra": "^10.0.0", + "json5": "^2.2.3", + "prompts": "^2.4.2" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-verify": "^2.1.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-ignition-ethers": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition-ethers/-/hardhat-ignition-ethers-0.15.17.tgz", + "integrity": "sha512-io6Wrp1dUsJ94xEI3pw6qkPfhc9TFA+e6/+o16yQ8pvBTFMjgK5x8wIHKrrIHr9L3bkuTMtmDjyN4doqO2IqFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "@nomicfoundation/hardhat-ignition": "^0.15.16", + "@nomicfoundation/ignition-core": "^0.15.15", + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-network-helpers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.1.2.tgz", + "integrity": "sha512-p7HaUVDbLj7ikFivQVNhnfMHUBgiHYMwQWvGn9AriieuopGOELIrwj2KjyM2a6z70zai5YKO264Vwz+3UFJZPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ethereumjs-util": "^7.1.4" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-toolbox": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-5.0.0.tgz", + "integrity": "sha512-FnUtUC5PsakCbwiVNsqlXVIWG5JIb5CEZoSXbJUsEBun22Bivx2jhF1/q9iQbzuaGpJKFQyOhemPB2+XlEE6pQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.0", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@types/node": ">=18.0.0", + "chai": "^4.2.0", + "ethers": "^6.4.0", + "hardhat": "^2.11.0", + "hardhat-gas-reporter": "^1.0.8", + "solidity-coverage": "^0.8.1", + "ts-node": ">=8.0.0", + "typechain": "^8.3.0", + "typescript": ">=4.5.0" + } + }, + "node_modules/@nomicfoundation/hardhat-verify": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.1.3.tgz", + "integrity": "sha512-danbGjPp2WBhLkJdQy9/ARM3WQIK+7vwzE0urNem1qZJjh9f54Kf5f1xuQv8DvqewUAkuPxVt/7q4Grz5WjqSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^8.1.0", + "debug": "^4.1.1", + "lodash.clonedeep": "^4.5.0", + "picocolors": "^1.1.0", + "semver": "^6.3.0", + "table": "^6.8.0", + "undici": "^5.14.0" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/ignition-core": { + "version": "0.15.15", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-core/-/ignition-core-0.15.15.tgz", + "integrity": "sha512-JdKFxYknTfOYtFXMN6iFJ1vALJPednuB+9p9OwGIRdoI6HYSh4ZBzyRURgyXtHFyaJ/SF9lBpsYV9/1zEpcYwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/address": "5.6.1", + "@nomicfoundation/solidity-analyzer": "^0.1.1", + "cbor": "^9.0.0", + "debug": "^4.3.2", + "ethers": "^6.14.0", + "fs-extra": "^10.0.0", + "immer": "10.0.2", + "lodash": "4.17.21", + "ndjson": "2.0.0" + } + }, + "node_modules/@nomicfoundation/ignition-core/node_modules/@ethersproject/address": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.6.1.tgz", + "integrity": "sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bignumber": "^5.6.2", + "@ethersproject/bytes": "^5.6.1", + "@ethersproject/keccak256": "^5.6.1", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/rlp": "^5.6.1" + } + }, + "node_modules/@nomicfoundation/ignition-core/node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@nomicfoundation/ignition-ui": { + "version": "0.15.13", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-ui/-/ignition-ui-0.15.13.tgz", + "integrity": "sha512-HbTszdN1iDHCkUS9hLeooqnLEW2U45FaqFwFEYT8nIno2prFZhG+n68JEERjmfFCB5u0WgbuJwk3CgLoqtSL7Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@nomicfoundation/solidity-analyzer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", + "integrity": "sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "optionalDependencies": { + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.2", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.2" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz", + "integrity": "sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz", + "integrity": "sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz", + "integrity": "sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz", + "integrity": "sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", + "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", + "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", + "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sentry/core": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", + "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/hub": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", + "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/minimal": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", + "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/node": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", + "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/core": "5.30.0", + "@sentry/hub": "5.30.0", + "@sentry/tracing": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/tracing": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", + "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/tracing/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@solidity-parser/parser": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", + "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typechain/ethers-v6": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", + "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "ethers": "6.x", + "typechain": "^8.3.2", + "typescript": ">=4.7.0" + } + }, + "node_modules/@typechain/hardhat": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-9.1.0.tgz", + "integrity": "sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fs-extra": "^9.1.0" + }, + "peerDependencies": { + "@typechain/ethers-v6": "^0.5.1", + "ethers": "^6.1.0", + "hardhat": "^2.9.9", + "typechain": "^8.3.2" + } + }, + "node_modules/@typechain/hardhat/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/secp256k1": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.7.tgz", + "integrity": "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "dev": true, + "license": "BSD-3-Clause OR MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4ts": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", + "dev": true, + "license": "WTFPL", + "peer": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/cli-table3/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/command-line-usage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/command-line-usage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/command-line-usage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/death": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/death/-/death-1.1.0.tgz", + "integrity": "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==", + "dev": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "dev": true, + "peer": true, + "dependencies": { + "heap": ">= 0.2.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=0.12.0" + }, + "optionalDependencies": { + "source-map": "~0.2.0" + } + }, + "node_modules/esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eth-gas-reporter": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.2.27.tgz", + "integrity": "sha512-femhvoAM7wL0GcI8ozTdxfuBtBFJ9qsyIAsmKVjlWAHUbdnnXHt+lKzz/kmldM5lA9jLuNHGwuIxorNpLbR1Zw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@solidity-parser/parser": "^0.14.0", + "axios": "^1.5.1", + "cli-table3": "^0.5.0", + "colors": "1.4.0", + "ethereum-cryptography": "^1.0.3", + "ethers": "^5.7.2", + "fs-readdir-recursive": "^1.1.0", + "lodash": "^4.17.14", + "markdown-table": "^1.1.3", + "mocha": "^10.2.0", + "req-cwd": "^2.0.0", + "sha1": "^1.1.1", + "sync-request": "^6.0.0" + }, + "peerDependencies": { + "@codechecks/client": "^0.1.0" + }, + "peerDependenciesMeta": { + "@codechecks/client": { + "optional": true + } + } + }, + "node_modules/eth-gas-reporter/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/eth-gas-reporter/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/eth-gas-reporter/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/eth-gas-reporter/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/eth-gas-reporter/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "node_modules/eth-gas-reporter/node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/ethereum-bloom-filters": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.2.0.tgz", + "integrity": "sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "^1.4.0" + } + }, + "node_modules/ethereum-bloom-filters/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", + "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/pbkdf2": "^3.0.0", + "@types/secp256k1": "^4.0.1", + "blakejs": "^1.1.0", + "browserify-aes": "^1.2.0", + "bs58check": "^2.1.2", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "hash.js": "^1.1.7", + "keccak": "^3.0.0", + "pbkdf2": "^3.0.17", + "randombytes": "^2.1.0", + "safe-buffer": "^5.1.2", + "scrypt-js": "^3.0.0", + "secp256k1": "^4.0.1", + "setimmediate": "^1.0.5" + } + }, + "node_modules/ethereumjs-util": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", + "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/ethjs-unit/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fp-ts": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", + "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ghost-testrpc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz", + "integrity": "sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "chalk": "^2.4.2", + "node-emoji": "^1.10.0" + }, + "bin": { + "testrpc-sc": "index.js" + } + }, + "node_modules/ghost-testrpc/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ghost-testrpc/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ghost-testrpc/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globby/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hardhat": { + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.28.6.tgz", + "integrity": "sha512-zQze7qe+8ltwHvhX5NQ8sN1N37WWZGw8L63y+2XcPxGwAjc/SMF829z3NS6o1krX0sryhAsVBK/xrwUqlsot4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@ethersproject/abi": "^5.1.2", + "@nomicfoundation/edr": "0.12.0-next.23", + "@nomicfoundation/solidity-analyzer": "^0.1.0", + "@sentry/node": "^5.18.1", + "adm-zip": "^0.4.16", + "aggregate-error": "^3.0.0", + "ansi-escapes": "^4.3.0", + "boxen": "^5.1.2", + "chokidar": "^4.0.0", + "ci-info": "^2.0.0", + "debug": "^4.1.1", + "enquirer": "^2.3.0", + "env-paths": "^2.2.0", + "ethereum-cryptography": "^1.0.3", + "find-up": "^5.0.0", + "fp-ts": "1.19.3", + "fs-extra": "^7.0.1", + "immutable": "^4.0.0-rc.12", + "io-ts": "1.10.4", + "json-stream-stringify": "^3.1.4", + "keccak": "^3.0.2", + "lodash": "^4.17.11", + "micro-eth-signer": "^0.14.0", + "mnemonist": "^0.38.0", + "mocha": "^10.0.0", + "p-map": "^4.0.0", + "picocolors": "^1.1.0", + "raw-body": "^2.4.1", + "resolve": "1.17.0", + "semver": "^6.3.0", + "solc": "0.8.26", + "source-map-support": "^0.5.13", + "stacktrace-parser": "^0.1.10", + "tinyglobby": "^0.2.6", + "tsort": "0.0.1", + "undici": "^5.14.0", + "uuid": "^8.3.2", + "ws": "^7.4.6" + }, + "bin": { + "hardhat": "internal/cli/bootstrap.js" + }, + "peerDependencies": { + "ts-node": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/hardhat-gas-reporter": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.10.tgz", + "integrity": "sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-uniq": "1.0.3", + "eth-gas-reporter": "^0.2.25", + "sha1": "^1.1.1" + }, + "peerDependencies": { + "hardhat": "^2.0.2" + } + }, + "node_modules/hardhat/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/hardhat/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "node_modules/hardhat/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/hardhat/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/hardhat/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/hardhat/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.2.tgz", + "integrity": "sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/io-ts": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", + "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fp-ts": "^1.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stream-stringify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz", + "integrity": "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=7.10.1" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micro-eth-signer": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz", + "integrity": "sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "micro-packed": "~0.7.2" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/curves": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.2" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/hashes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/micro-packed": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.7.3.tgz", + "integrity": "sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mnemonist": { + "version": "0.38.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", + "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.0" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ndjson": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz", + "integrity": "sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "json-stringify-safe": "^5.0.1", + "minimist": "^1.2.5", + "readable-stream": "^3.6.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "ndjson": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/number-to-bn/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordinal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", + "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true, + "peer": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "peer": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/recursive-readdir/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/recursive-readdir/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/req-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", + "integrity": "sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "req-from": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/req-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-from/-/req-from-2.0.0.tgz", + "integrity": "sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rlp": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz", + "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "bn.js": "^5.2.0" + }, + "bin": { + "rlp": "bin/rlp" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sc-istanbul": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/sc-istanbul/-/sc-istanbul-0.4.6.tgz", + "integrity": "sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "istanbul": "lib/cli.js" + } + }, + "node_modules/sc-istanbul/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/sc-istanbul/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sc-istanbul/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sc-istanbul/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sc-istanbul/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/sc-istanbul/node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sc-istanbul/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sc-istanbul/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/sc-istanbul/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/secp256k1": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", + "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shelljs/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/solc": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", + "integrity": "sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/solidity-coverage": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.17.tgz", + "integrity": "sha512-5P8vnB6qVX9tt1MfuONtCTEaEGO/O4WuEidPHIAJjx4sktHHKhO3rFvnE0q8L30nWJPTrcqGQMT7jpE29B2qow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.0.9", + "@solidity-parser/parser": "^0.20.1", + "chalk": "^2.4.2", + "death": "^1.1.0", + "difflib": "^0.2.4", + "fs-extra": "^8.1.0", + "ghost-testrpc": "^0.0.2", + "global-modules": "^2.0.0", + "globby": "^10.0.1", + "jsonschema": "^1.2.4", + "lodash": "^4.17.21", + "mocha": "^10.2.0", + "node-emoji": "^1.10.0", + "pify": "^4.0.1", + "recursive-readdir": "^2.2.2", + "sc-istanbul": "^0.4.5", + "semver": "^7.3.4", + "shelljs": "^0.8.3", + "web3-utils": "^1.3.6" + }, + "bin": { + "solidity-coverage": "plugins/bin.js" + }, + "peerDependencies": { + "hardhat": "^2.11.0" + } + }, + "node_modules/solidity-coverage/node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/solidity-coverage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/solidity-coverage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/solidity-coverage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/solidity-coverage/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/solidity-coverage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/solidity-coverage/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solidity-coverage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true, + "license": "WTFPL OR MIT", + "peer": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-port": "^3.1.0" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/then-request/node_modules/@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/then-request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" + }, + "bin": { + "write-markdown": "dist/write-markdown.js" + } + }, + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "typescript": ">=3.7.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD", + "peer": true + }, + "node_modules/tsort": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", + "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typechain": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" + } + }, + "node_modules/typechain/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/typechain/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typechain/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/typechain/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typechain/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typechain/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web3-utils": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", + "dev": true, + "license": "LGPL-3.0", + "peer": true, + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereum-cryptography": "^2.1.2", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/web3-utils/node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web3-utils/node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web3-utils/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..35f6180 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "poc-openrouter", + "version": "1.0.0", + "private": true, + "scripts": { + "compile": "hardhat compile", + "deploy:v2": "hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network", + "typechain": "hardhat typechain" + }, + "devDependencies": { + "@nomicfoundation/hardhat-foundry": "^1.1.2", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "dotenv": "^16.0.0", + "hardhat": "^2.22.7", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..fe98d3e --- /dev/null +++ b/remappings.txt @@ -0,0 +1,2 @@ +solady/=lib/solady/ +forge-std/=lib/forge-std/src/ diff --git a/scripts/deploy/deployBungeeOpenRouterV2.ts b/scripts/deploy/deployBungeeOpenRouterV2.ts new file mode 100644 index 0000000..3744b7b --- /dev/null +++ b/scripts/deploy/deployBungeeOpenRouterV2.ts @@ -0,0 +1,79 @@ +/** + * Deployment script for BungeeOpenRouterV2 and BungeeOpenRouterV2Unchecked. + * + * Usage: + * npx hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network + * + * Required env vars: + * DEPLOYER_PRIVATE_KEY — deployer wallet private key + * OWNER_ADDRESS — owner of both contracts (defaults to deployer) + * OPEN_ROUTER_SIGNER_ADDRESS — backend signer for BungeeOpenRouterV2 + * + * Optional: set --network to any network configured in hardhat.config.ts. + * Omitting --network runs against the in-process Hardhat network. + */ + +import { ethers } from "hardhat"; + +async function main() { + const [deployer] = await ethers.getSigners(); + + const owner = process.env.OWNER_ADDRESS ?? deployer.address; + const openRouterSigner = process.env.OPEN_ROUTER_SIGNER_ADDRESS; + + if (!openRouterSigner) { + throw new Error("OPEN_ROUTER_SIGNER_ADDRESS is not set in environment"); + } + + console.log("Deployer: ", deployer.address); + console.log("Owner: ", owner); + console.log("OpenRouterSigner: ", openRouterSigner); + console.log("Network: ", (await ethers.provider.getNetwork()).name); + console.log(""); + + // ------------------------------------------------------------------------- + // BungeeOpenRouterV2 (monolithic + modular, signature-verified, AH pull) + // ------------------------------------------------------------------------- + console.log("Deploying BungeeOpenRouterV2..."); + const V2Factory = await ethers.getContractFactory("BungeeOpenRouterV2"); + const v2 = await V2Factory.deploy(owner, openRouterSigner); + await v2.waitForDeployment(); + const v2Address = await v2.getAddress(); + console.log("BungeeOpenRouterV2 deployed to:", v2Address); + + // ------------------------------------------------------------------------- + // BungeeOpenRouterV2Unchecked (same logic, no signature verification) + // ------------------------------------------------------------------------- + console.log("Deploying BungeeOpenRouterV2Unchecked..."); + const V2UFactory = await ethers.getContractFactory("BungeeOpenRouterV2Unchecked"); + const v2u = await V2UFactory.deploy(owner); + await v2u.waitForDeployment(); + const v2uAddress = await v2u.getAddress(); + console.log("BungeeOpenRouterV2Unchecked deployed to:", v2uAddress); + + // ------------------------------------------------------------------------- + // Summary + // ------------------------------------------------------------------------- + console.log("\n=== Deployment Summary ==="); + console.log(`BungeeOpenRouterV2: ${v2Address}`); + console.log(`BungeeOpenRouterV2Unchecked: ${v2uAddress}`); + + // ------------------------------------------------------------------------- + // Verification hint + // ------------------------------------------------------------------------- + const chainId = (await ethers.provider.getNetwork()).chainId; + if (chainId !== 31337n) { + console.log("\nTo verify on a block explorer:"); + console.log( + ` npx hardhat verify --network ${v2Address} "${owner}" "${openRouterSigner}"` + ); + console.log( + ` npx hardhat verify --network ${v2uAddress} "${owner}"` + ); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6257b56 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "strict": true, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": ["scripts/**/*", "hardhat.config.ts"], + "files": ["hardhat.config.ts"] +} From c12f2e3e406e592e84946b52d75ed4129c11870b Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 12 May 2026 01:30:29 +0400 Subject: [PATCH 06/69] feat: gas measure and optimise --- src/dummyRouter.sol | 55 +++++++++++++------ ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 3 + 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/dummyRouter.sol b/src/dummyRouter.sol index aab54cc..715914e 100644 --- a/src/dummyRouter.sol +++ b/src/dummyRouter.sol @@ -28,47 +28,68 @@ contract DummyRouter { error MissingNativeValue(uint256 actionIndex); function execute(Action[] calldata actions) external payable returns (bytes[] memory results) { - results = new bytes[](actions.length); + uint256 actionsLength = actions.length; + results = new bytes[](actionsLength); - for (uint256 i = 0; i < actions.length; i++) { - bytes memory callData = actions[i].data; + for (uint256 i; i < actionsLength;) { + Action calldata action = actions[i]; + bytes memory callData = action.data; // Patch this action's calldata using earlier action results. - for (uint256 j = 0; j < actions[i].splices.length; j++) { - Splice calldata s = actions[i].splices[j]; - if (s.sourceActionIndex >= i) revert FutureSplice(i, s.sourceActionIndex); + uint256 splicesLength = action.splices.length; + for (uint256 j; j < splicesLength;) { + Splice calldata s = action.splices[j]; + uint256 sourceActionIndex = s.sourceActionIndex; + if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); - bytes memory source = results[s.sourceActionIndex]; - if (s.srcOffset + s.length > source.length || s.dstOffset + s.length > callData.length) { + uint256 length = s.length; + uint256 srcOffset = s.srcOffset; + uint256 dstOffset = s.dstOffset; + bytes memory source = results[sourceActionIndex]; + if (srcOffset + length > source.length || dstOffset + length > callData.length) { revert SpliceOutOfBounds(i, j); } - for (uint256 k = 0; k < s.length; k++) { - callData[s.dstOffset + k] = source[s.srcOffset + k]; + assembly ("memory-safe") { + mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) + } + + unchecked { + ++j; } } bool success; bytes memory ret; + CallType callType = action.callType; + address target = action.target; - if (actions[i].callType == CallType.STATICCALL) { - (success, ret) = actions[i].target.staticcall(callData); - } else if (actions[i].callType == CallType.CALL_WITH_NATIVE) { + if (callType == CallType.STATICCALL) { + (success, ret) = target.staticcall(callData); + } else if (callType == CallType.CALL_WITH_NATIVE) { if (callData.length < 32) revert MissingNativeValue(i); uint256 callValue; - bytes memory payload = new bytes(callData.length - 32); + uint256 payloadLength = callData.length - 32; assembly ("memory-safe") { callValue := mload(add(callData, 0x20)) - mcopy(add(payload, 0x20), add(callData, 0x40), mload(payload)) + success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) + + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) + returndatacopy(add(ret, 0x20), 0, returnDataSize) + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) } - (success, ret) = actions[i].target.call{value: callValue}(payload); } else { - (success, ret) = actions[i].target.call(callData); + (success, ret) = target.call(callData); } if (!success) revert CallFailed(i, ret); results[i] = ret; + unchecked { + ++i; + } } } diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index 2cef834..3139f44 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -100,7 +100,10 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { manipulator, inputAmount, nativeFee, _openOceanSwapCalldata(inputAmount), _stargateCalldata(nativeFee) ); + uint256 gasBeforeExecute = gasleft(); bytes[] memory results = router.execute(actions); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("router.execute gas used", executeGasUsed); _assertPocResult( router, From a1bfeea1e9bc182dadc8fdddef965909f4d70eaf Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 12 May 2026 02:01:21 +0400 Subject: [PATCH 07/69] feat: swapFeeBridge router --- src/swapFeeBridgeRouter.sol | 66 +++++ ...StargateNativeSwapFeeBridgeRouterPoC.t.sol | 241 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 src/swapFeeBridgeRouter.sol create mode 100644 test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol diff --git a/src/swapFeeBridgeRouter.sol b/src/swapFeeBridgeRouter.sol new file mode 100644 index 0000000..efe93ab --- /dev/null +++ b/src/swapFeeBridgeRouter.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.25; + +contract SwapFeeBridgeRouter { + bytes4 internal constant APPROVE_SELECTOR = 0x095ea7b3; + uint256 internal constant BPS_DENOMINATOR = 10_000; + + struct SwapFeeBridgeParams { + address inputToken; + address approveTarget; + uint256 inputAmount; + address swapTarget; + bytes swapData; + address bridgeTarget; + bytes bridgeData; + uint256 bridgeAmountOffset; + address feeRecipient; + uint256 feeBps; + uint256 nativeFee; + } + + error ApproveFailed(bytes returndata); + error SwapFailed(bytes returndata); + error FeeTransferFailed(bytes returndata); + error BridgeCalldataOutOfBounds(uint256 offset, uint256 length); + error BridgeFailed(bytes returndata); + + function swapFeeBridge(SwapFeeBridgeParams calldata params) + external + payable + returns (uint256 swapOutput, uint256 routeFee, uint256 postFeeAmount, uint256 bridgeAmount) + { + bool success; + bytes memory returndata; + + (success, returndata) = + params.inputToken.call(abi.encodeWithSelector(APPROVE_SELECTOR, params.approveTarget, params.inputAmount)); + if (!success) revert ApproveFailed(returndata); + + (success, returndata) = params.swapTarget.call(params.swapData); + if (!success) revert SwapFailed(returndata); + + swapOutput = abi.decode(returndata, (uint256)); + routeFee = swapOutput * params.feeBps / BPS_DENOMINATOR; + postFeeAmount = swapOutput - routeFee; + bridgeAmount = postFeeAmount - params.nativeFee; + + (success, returndata) = params.feeRecipient.call{value: routeFee}(""); + if (!success) revert FeeTransferFailed(returndata); + + bytes memory bridgeData = params.bridgeData; + uint256 bridgeAmountOffset = params.bridgeAmountOffset; + if (bridgeData.length < 32 || bridgeAmountOffset > bridgeData.length - 32) { + revert BridgeCalldataOutOfBounds(bridgeAmountOffset, bridgeData.length); + } + + assembly ("memory-safe") { + mstore(add(add(bridgeData, 0x20), bridgeAmountOffset), bridgeAmount) + } + + (success, returndata) = params.bridgeTarget.call{value: postFeeAmount}(bridgeData); + if (!success) revert BridgeFailed(returndata); + } + + receive() external payable {} +} diff --git a/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol new file mode 100644 index 0000000..2043497 --- /dev/null +++ b/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {SwapFeeBridgeRouter} from "../../src/swapFeeBridgeRouter.sol"; + +interface ISwapFeeBridgeOpenOceanExchangeV2 { + struct SwapDescription { + address srcToken; + address dstToken; + address srcReceiver; + address dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + address referrer; + bytes permit; + } + + struct CallDescription { + uint256 target; + uint256 gasLimit; + uint256 value; + bytes data; + } +} + +interface ISwapFeeBridgeStargateNative { + struct SendParam { + uint32 dstEid; + bytes32 to; + uint256 amountLD; + uint256 minAmountLD; + bytes extraOptions; + bytes composeMsg; + bytes oftCmd; + } + + struct MessagingFee { + uint256 nativeFee; + uint256 lzTokenFee; + } + + function send(SendParam calldata sendParam, MessagingFee calldata fee, address refundAddress) external payable; +} + +// ref tx 0xef65dc3323cd757c5e3a1a872b99beff6e71f0a80b1a2a6d280d2f2458f3cbaf +contract OpenOceanStargateNativeSwapFeeBridgeRouterPoCTest is Test { + bytes4 internal constant OPENOCEAN_SWAP_SELECTOR = 0x0a9704d5; + address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant OPENOCEAN_CALLER = 0xB100a5B2591Dd099040a5ab76EFe682A6D8a48a2; + address internal constant OPENOCEAN_REFERRER = 0x38c7720238a2C123814aaF1A3D0e31E0093aF046; + address internal constant STARGATE_NATIVE_WRAPPER = 0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant FEE_RECIPIENT = 0x0079a23EDEA601190EdF1cda05c8Af3fEA2f2d9F; + address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + address internal constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + uint256 internal constant FORK_BLOCK_NUMBER = 461_745_499; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01f38b; + uint256 internal constant SWAP_INPUT_USDC = 0x1312d00; + uint256 internal constant OPENOCEAN_MIN_RETURN = 0x1b91a33e163bdf; + uint256 internal constant OPENOCEAN_FLAGS = 2; + uint256 internal constant STARGATE_NATIVE_FEE = 0x1603e90a5fe0; + uint256 internal constant ROUTE_FEE_BPS = 100; + + uint32 internal constant BASE_ENDPOINT_ID = 30_184; + uint256 internal constant STARGATE_AMOUNT_OFFSET = 196; + + function test_openOceanSwapFeeBridgeRouter_arbitrumFork() public { + string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("ARBITRUM_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + SwapFeeBridgeRouter router = _routerAtFixtureAddress(); + if (bytes(rpcUrl).length == 0) { + emit log("Set ARBITRUM_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_USDC_AMOUNT", SWAP_INPUT_USDC); + uint256 nativeFee = vm.envOr("POC_STARGATE_NATIVE_FEE", STARGATE_NATIVE_FEE); + + deal(ARBITRUM_USDC, address(router), inputAmount); + uint256 initialNativeBalance = address(router).balance; + uint256 initialFeeRecipientBalance = FEE_RECIPIENT.balance; + uint256 initialWethBalance = ERC20(ARBITRUM_WETH).balanceOf(address(router)); + + SwapFeeBridgeRouter.SwapFeeBridgeParams memory params = SwapFeeBridgeRouter.SwapFeeBridgeParams({ + inputToken: ARBITRUM_USDC, + approveTarget: OPENOCEAN_EXCHANGE_V2, + inputAmount: inputAmount, + swapTarget: OPENOCEAN_EXCHANGE_V2, + swapData: _openOceanSwapCalldata(inputAmount), + bridgeTarget: STARGATE_NATIVE_WRAPPER, + bridgeData: _stargateCalldata(nativeFee), + bridgeAmountOffset: STARGATE_AMOUNT_OFFSET, + feeRecipient: FEE_RECIPIENT, + feeBps: ROUTE_FEE_BPS, + nativeFee: nativeFee + }); + + uint256 gasBeforeSwapFeeBridge = gasleft(); + (uint256 swapOutput, uint256 routeFee, uint256 postFeeAmount, uint256 bridgeAmount) = + router.swapFeeBridge(params); + uint256 swapFeeBridgeGasUsed = gasBeforeSwapFeeBridge - gasleft(); + emit log_named_uint("router.swapFeeBridge gas used", swapFeeBridgeGasUsed); + + _assertPocResult( + router, + nativeFee, + initialNativeBalance, + initialFeeRecipientBalance, + initialWethBalance, + swapOutput, + routeFee, + postFeeAmount, + bridgeAmount + ); + } + + function _openOceanSwapCalldata(uint256 inputAmount) internal pure returns (bytes memory) { + return abi.encodeWithSelector( + OPENOCEAN_SWAP_SELECTOR, + OPENOCEAN_CALLER, + ISwapFeeBridgeOpenOceanExchangeV2.SwapDescription({ + srcToken: ARBITRUM_USDC, + dstToken: NATIVE_TOKEN, + srcReceiver: OPENOCEAN_CALLER, + dstReceiver: FIXTURE_ROUTER, + amount: inputAmount, + minReturnAmount: OPENOCEAN_MIN_RETURN, + flags: OPENOCEAN_FLAGS, + referrer: OPENOCEAN_REFERRER, + permit: "" + }), + _openOceanCalls() + ); + } + + function _stargateCalldata(uint256 nativeFee) internal pure returns (bytes memory) { + return abi.encodeCall( + ISwapFeeBridgeStargateNative.send, + ( + ISwapFeeBridgeStargateNative.SendParam({ + dstEid: BASE_ENDPOINT_ID, + to: _toBytes32(FIXTURE_RECIPIENT), + amountLD: 0, + minAmountLD: 0, + extraOptions: "", + composeMsg: "", + oftCmd: "" + }), + ISwapFeeBridgeStargateNative.MessagingFee({nativeFee: nativeFee, lzTokenFee: 0}), + FIXTURE_RECIPIENT + ) + ); + } + + function _openOceanCalls() + internal + pure + returns (ISwapFeeBridgeOpenOceanExchangeV2.CallDescription[] memory calls) + { + calls = new ISwapFeeBridgeOpenOceanExchangeV2.CallDescription[](6); + calls[0] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b9470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112a880000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab1000003000000000000000000000000000000000000" + }); + calls[1] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af9508000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + calls[2] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ + target: uint256(uint160(0xb7236B927e03542AC3bE0A054F2bEa8868AF9508)), + gasLimit: 0, + value: 0, + data: hex"53c059a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2" + }); + calls[3] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000400000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + calls[4] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"8a6a1e85000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000000000000000000000000000000000000000000000000001ea1d1d3352615" + }); + calls[5] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f865422000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + } + + function _assertPocResult( + SwapFeeBridgeRouter router, + uint256 nativeFee, + uint256 initialNativeBalance, + uint256 initialFeeRecipientBalance, + uint256 initialWethBalance, + uint256 swapOutput, + uint256 routeFee, + uint256 postFeeAmount, + uint256 bridgeAmount + ) internal view { + assertGt(swapOutput, 0); + assertEq(routeFee, swapOutput * ROUTE_FEE_BPS / 10_000); + assertEq(FEE_RECIPIENT.balance - initialFeeRecipientBalance, routeFee); + assertEq(postFeeAmount + routeFee, swapOutput); + assertEq(bridgeAmount + nativeFee, postFeeAmount); + assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); + assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), initialWethBalance); + assertLt(address(router).balance - initialNativeBalance, nativeFee); + } + + function _routerAtFixtureAddress() internal returns (SwapFeeBridgeRouter router) { + SwapFeeBridgeRouter implementation = new SwapFeeBridgeRouter(); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return SwapFeeBridgeRouter(payable(FIXTURE_ROUTER)); + } + + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} From ec8a74dc7d0a3db75adb73767323cb8ff0db2659 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 13:13:44 +0530 Subject: [PATCH 08/69] feat: native token support --- src/combined/BungeeOpenRouterV2.sol | 36 +++++++++++++++++--- src/combined/BungeeOpenRouterV2Unchecked.sol | 33 +++++++++++++++--- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol index 6f55732..daa550d 100644 --- a/src/combined/BungeeOpenRouterV2.sol +++ b/src/combined/BungeeOpenRouterV2.sol @@ -73,6 +73,10 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { uint256 value; // ETH forwarded to the bridge target bytes data; uint256[] amountPositions; + // when true, bridge.value is ignored and finalAmount is forwarded as + // msg.value instead — needed for native-token bridges (e.g. Arbitrum inbox) + // where the bridged amount is only known at runtime. + bool useFinalAmountAsValue; } /// @notice Signed payload for the monolithic execution path. @@ -129,6 +133,7 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { error InsufficientFunds(); error InvalidExecution(); error CallerNotSignedUser(); + error InsufficientMsgValue(); error ValueOnNonCall(); error EmptyActions(); error UnknownCallType(); @@ -228,7 +233,10 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { } // 7. bridge call, bubbling any revert - _performAction(exec.bridge.target, exec.bridge.value, bridgeData); + // when useFinalAmountAsValue is set, forward finalAmount as msg.value so + // native-token bridges (e.g. Arbitrum inbox) receive the exact bridged amount. + uint256 bridgeValue = exec.bridge.useFinalAmountAsValue ? finalAmount : exec.bridge.value; + _performAction(exec.bridge.target, bridgeValue, bridgeData); } /// @dev Balance-delta swap helper; split out to keep _runMonolithic under 100 lines. @@ -266,13 +274,22 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { // ========================================================================= /** - * @notice Pulls `amount` of `token` from `user` via AllowanceHolder. - * @dev Requires the caller to have routed through `AllowanceHolder.exec` - * so `_msgSender()` resolves to the original user. Mirrors the - * assembly in `0x-settler/src/core/Permit2Payment.sol`. + * @notice Pulls `amount` of `token` from `user` into this contract. + * @dev For ERC20: enforces `_msgSender() == user` (caller must have routed + * through `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea + * For native ETH: ETH must already be present as msg.value; we simply + * verify sufficient value was forwarded. No AH call is needed. */ function _pullFromUser(address token, address user, uint256 amount) internal { + if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { + // ETH is already sent as msg.value directly to this contract. + if (msg.value < amount) { + revert InsufficientMsgValue(); + } + return; + } + if (_msgSender() != user) { revert CallerNotSignedUser(); } @@ -340,11 +357,20 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { * @dev Named `_dispatchAction` (rather than overloading `_performAction`) * to keep the CALL-only base helper in `OpenRouterAuthBase` distinct * from this three-way dispatcher. + * + * `value == type(uint256).max` is a sentinel meaning "use entire contract + * ETH balance". This lets modular callers forward the full native output + * of a swap to a native-token bridge without knowing the exact amount at + * calldata-build time. */ function _dispatchAction(CallType callType, address target, uint256 value, bytes memory data) internal returns (bytes memory ret) { + if (value == type(uint256).max) { + value = address(this).balance; + } + bool ok; if (callType == CallType.CALL) { (ok, ret) = target.call{value: value}(data); diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index ea99d59..dca08f5 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -62,6 +62,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { uint256 value; bytes data; uint256[] amountPositions; + // when true, bridge.value is ignored and finalAmount is forwarded as + // msg.value instead — needed for native-token bridges (e.g. Arbitrum inbox) + // where the bridged amount is only known at runtime. + bool useFinalAmountAsValue; } struct MonolithicExecution { @@ -104,6 +108,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { error InsufficientFunds(); error InvalidExecution(); error CallerNotSignedUser(); + error InsufficientMsgValue(); error ValueOnNonCall(); error EmptyActions(); error UnknownCallType(); @@ -197,7 +202,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // 7. bridge call, bubbling any revert - _doCall(exec.bridge.target, exec.bridge.value, bridgeData); + // when useFinalAmountAsValue is set, forward finalAmount as msg.value so + // native-token bridges (e.g. Arbitrum inbox) receive the exact bridged amount. + uint256 bridgeValue = exec.bridge.useFinalAmountAsValue ? finalAmount : exec.bridge.value; + _doCall(exec.bridge.target, bridgeValue, bridgeData); } /// @dev Balance-delta swap helper. @@ -235,12 +243,22 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // ========================================================================= /** - * @notice Pulls `amount` of `token` from `user` via AllowanceHolder. - * @dev Enforces `_msgSender() == user`: the caller must have routed through - * `AllowanceHolder.exec` whose `owner` argument matches `user`. + * @notice Pulls `amount` of `token` from `user` into this contract. + * @dev For ERC20: enforces `_msgSender() == user` (caller must have routed + * through `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea + * For native ETH: ETH must already be present as msg.value; we simply + * verify sufficient value was forwarded. No AH call is needed. */ function _pullFromUser(address token, address user, uint256 amount) internal { + if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { + // ETH is already sent as msg.value directly to this contract. + if (msg.value < amount) { + revert InsufficientMsgValue(); + } + return; + } + if (_msgSender() != user) { revert CallerNotSignedUser(); } @@ -302,6 +320,13 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { internal returns (bytes memory ret) { + // type(uint256).max is a sentinel meaning "use entire contract ETH balance". + // This lets modular callers forward the full native output of a swap to a + // native-token bridge without knowing the exact amount at calldata-build time. + if (value == type(uint256).max) { + value = address(this).balance; + } + bool ok; if (callType == CallType.CALL) { (ok, ret) = target.call{value: value}(data); From a9d6b1e03a40b33ab519c0735587e1a35f948de6 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 13:14:23 +0530 Subject: [PATCH 09/69] test: test scripts --- .env.example | 7 + package-lock.json | 142 ++++--- package.json | 3 + scripts/deploy/deployBungeeOpenRouterV2.ts | 52 +-- scripts/e2e/bridgeViaRelay.ts | 343 +++++++++++++++++ scripts/e2e/config.ts | 96 +++++ scripts/e2e/swapBridgeViaArbitrumNative.ts | 411 ++++++++++++++++++++ scripts/e2e/swapBridgeViaCctp.ts | 415 +++++++++++++++++++++ scripts/e2e/utils/allowanceHolder.ts | 65 ++++ scripts/e2e/utils/contractTypes.ts | 87 +++++ scripts/e2e/utils/erc20.ts | 87 +++++ scripts/e2e/utils/routerAbi.ts | 21 ++ 12 files changed, 1650 insertions(+), 79 deletions(-) create mode 100644 scripts/e2e/bridgeViaRelay.ts create mode 100644 scripts/e2e/config.ts create mode 100644 scripts/e2e/swapBridgeViaArbitrumNative.ts create mode 100644 scripts/e2e/swapBridgeViaCctp.ts create mode 100644 scripts/e2e/utils/allowanceHolder.ts create mode 100644 scripts/e2e/utils/contractTypes.ts create mode 100644 scripts/e2e/utils/erc20.ts create mode 100644 scripts/e2e/utils/routerAbi.ts diff --git a/.env.example b/.env.example index 35e585b..528f41e 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,17 @@ # Private key of the deployer wallet (no 0x prefix) DEPLOYER_PRIVATE_KEY= +# Private key used by e2e scripts (may be the same as DEPLOYER_PRIVATE_KEY) +PRIVATE_KEY= + # Constructor arguments OWNER_ADDRESS= OPEN_ROUTER_SIGNER_ADDRESS= # only needed for BungeeOpenRouterV2 (not Unchecked) +# External API keys +RELAY_API_KEY= # optional, relay.link x-api-key header +OPEN_OCEAN_API_KEY= # optional, OpenOcean API key + # RPC endpoints (public fallbacks are pre-configured in hardhat.config.ts) ETHEREUM_RPC= POLYGON_RPC= diff --git a/package-lock.json b/package-lock.json index f3fbd15..a4813d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,12 @@ "name": "poc-openrouter", "version": "1.0.0", "devDependencies": { + "@arbitrum/sdk": "^4.0.5", "@nomicfoundation/hardhat-foundry": "^1.1.2", "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "axios": "^1.16.0", "dotenv": "^16.0.0", + "ethers": "^6.16.0", "hardhat": "^2.22.7", "ts-node": "^10.9.0", "typescript": "^5.0.0" @@ -21,8 +24,75 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "dev": true, + "license": "MIT" + }, + "node_modules/@arbitrum/sdk": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@arbitrum/sdk/-/sdk-4.0.5.tgz", + "integrity": "sha512-bADi4kVzSBUAV+GkxuKMx7zrkCVahIE4+fkBi0Ee18EPqGt1Wiub+yQCGTh+llApn1RpRtwwtYeZXhz9XelqGQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ethersproject/address": "^5.0.8", + "@ethersproject/bignumber": "^5.1.1", + "@ethersproject/bytes": "^5.0.8", + "async-mutex": "^0.4.0", + "ethers": "^5.1.0" + }, + "engines": { + "node": ">=v11", + "npm": ">=7", + "yarn": ">= 1.0.0" + } + }, + "node_modules/@arbitrum/sdk/node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], "license": "MIT", - "peer": true + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -241,7 +311,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/properties": "^5.8.0" @@ -325,7 +394,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abi": "^5.8.0", "@ethersproject/abstract-provider": "^5.8.0", @@ -383,7 +451,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/basex": "^5.8.0", @@ -415,7 +482,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", @@ -437,8 +503,7 @@ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@ethersproject/keccak256": { "version": "5.8.0", @@ -514,7 +579,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/sha2": "^5.8.0" @@ -556,7 +620,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", @@ -586,7 +649,6 @@ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -619,7 +681,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0" @@ -662,7 +723,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", @@ -710,7 +770,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", @@ -786,7 +845,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/constants": "^5.8.0", @@ -809,7 +867,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", @@ -868,7 +925,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/hash": "^5.8.0", @@ -921,7 +977,6 @@ "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@noble/hashes": "1.3.2" }, @@ -935,7 +990,6 @@ "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 16" }, @@ -1913,8 +1967,7 @@ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/agent-base": { "version": "6.0.2", @@ -2142,13 +2195,22 @@ "license": "MIT", "peer": true }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -2184,7 +2246,6 @@ "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -2214,8 +2275,7 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -2409,7 +2469,6 @@ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2727,7 +2786,6 @@ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3125,7 +3183,6 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4.0" } @@ -3196,7 +3253,6 @@ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -3266,7 +3322,6 @@ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -3277,7 +3332,6 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -3288,7 +3342,6 @@ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -3302,7 +3355,6 @@ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -3637,7 +3689,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", @@ -3657,7 +3708,6 @@ "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -3667,8 +3717,7 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ethjs-unit": { "version": "0.1.6", @@ -3867,7 +3916,6 @@ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3938,7 +3986,6 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3970,7 +4017,6 @@ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -4007,7 +4053,6 @@ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -4256,7 +4301,6 @@ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -4542,7 +4586,6 @@ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -4556,7 +4599,6 @@ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -4653,7 +4695,6 @@ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -5251,7 +5292,6 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -5372,7 +5412,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -5383,7 +5422,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5980,7 +6018,6 @@ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -6461,8 +6498,7 @@ "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/secp256k1": { "version": "4.0.4", @@ -7485,8 +7521,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsort": { "version": "0.0.1", @@ -8025,7 +8060,6 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 35f6180..5a28b2d 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,12 @@ "typechain": "hardhat typechain" }, "devDependencies": { + "@arbitrum/sdk": "^4.0.5", "@nomicfoundation/hardhat-foundry": "^1.1.2", "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "axios": "^1.16.0", "dotenv": "^16.0.0", + "ethers": "^6.16.0", "hardhat": "^2.22.7", "ts-node": "^10.9.0", "typescript": "^5.0.0" diff --git a/scripts/deploy/deployBungeeOpenRouterV2.ts b/scripts/deploy/deployBungeeOpenRouterV2.ts index 3744b7b..5d51a2e 100644 --- a/scripts/deploy/deployBungeeOpenRouterV2.ts +++ b/scripts/deploy/deployBungeeOpenRouterV2.ts @@ -13,49 +13,51 @@ * Omitting --network runs against the in-process Hardhat network. */ -import { ethers } from "hardhat"; +import { ethers } from 'hardhat'; async function main() { const [deployer] = await ethers.getSigners(); - const owner = process.env.OWNER_ADDRESS ?? deployer.address; - const openRouterSigner = process.env.OPEN_ROUTER_SIGNER_ADDRESS; + const owner = deployer.address; + const openRouterSigner = deployer.address; if (!openRouterSigner) { - throw new Error("OPEN_ROUTER_SIGNER_ADDRESS is not set in environment"); + throw new Error('OPEN_ROUTER_SIGNER_ADDRESS is not set in environment'); } - console.log("Deployer: ", deployer.address); - console.log("Owner: ", owner); - console.log("OpenRouterSigner: ", openRouterSigner); - console.log("Network: ", (await ethers.provider.getNetwork()).name); - console.log(""); + console.log('Deployer: ', deployer.address); + console.log('Owner: ', owner); + console.log('OpenRouterSigner: ', openRouterSigner); + console.log('Network: ', (await ethers.provider.getNetwork()).name); + console.log(''); // ------------------------------------------------------------------------- // BungeeOpenRouterV2 (monolithic + modular, signature-verified, AH pull) // ------------------------------------------------------------------------- - console.log("Deploying BungeeOpenRouterV2..."); - const V2Factory = await ethers.getContractFactory("BungeeOpenRouterV2"); - const v2 = await V2Factory.deploy(owner, openRouterSigner); - await v2.waitForDeployment(); - const v2Address = await v2.getAddress(); - console.log("BungeeOpenRouterV2 deployed to:", v2Address); + // console.log("Deploying BungeeOpenRouterV2..."); + // const V2Factory = await ethers.getContractFactory("BungeeOpenRouterV2"); + // const v2 = await V2Factory.deploy(owner, openRouterSigner); + // await v2.waitForDeployment(); + // const v2Address = await v2.getAddress(); + // console.log("BungeeOpenRouterV2 deployed to:", v2Address); // ------------------------------------------------------------------------- // BungeeOpenRouterV2Unchecked (same logic, no signature verification) // ------------------------------------------------------------------------- - console.log("Deploying BungeeOpenRouterV2Unchecked..."); - const V2UFactory = await ethers.getContractFactory("BungeeOpenRouterV2Unchecked"); + console.log('Deploying BungeeOpenRouterV2Unchecked...'); + const V2UFactory = await ethers.getContractFactory( + 'BungeeOpenRouterV2Unchecked', + ); const v2u = await V2UFactory.deploy(owner); await v2u.waitForDeployment(); const v2uAddress = await v2u.getAddress(); - console.log("BungeeOpenRouterV2Unchecked deployed to:", v2uAddress); + console.log('BungeeOpenRouterV2Unchecked deployed to:', v2uAddress); // ------------------------------------------------------------------------- // Summary // ------------------------------------------------------------------------- - console.log("\n=== Deployment Summary ==="); - console.log(`BungeeOpenRouterV2: ${v2Address}`); + console.log('\n=== Deployment Summary ==='); + // console.log(`BungeeOpenRouterV2: ${v2Address}`); console.log(`BungeeOpenRouterV2Unchecked: ${v2uAddress}`); // ------------------------------------------------------------------------- @@ -63,12 +65,12 @@ async function main() { // ------------------------------------------------------------------------- const chainId = (await ethers.provider.getNetwork()).chainId; if (chainId !== 31337n) { - console.log("\nTo verify on a block explorer:"); + console.log('\nTo verify on a block explorer:'); + // console.log( + // ` npx hardhat verify --network ${v2Address} "${owner}" "${openRouterSigner}"` + // ); console.log( - ` npx hardhat verify --network ${v2Address} "${owner}" "${openRouterSigner}"` - ); - console.log( - ` npx hardhat verify --network ${v2uAddress} "${owner}"` + ` npx hardhat verify --network ${v2uAddress} "${owner}"`, ); } } diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts new file mode 100644 index 0000000..7f19c45 --- /dev/null +++ b/scripts/e2e/bridgeViaRelay.ts @@ -0,0 +1,343 @@ +/** + * Script 1 — Bridge AAVE (Arbitrum) → PEPE (Base) via Relay.link + * + * Flow: + * 1. Fetch a Relay.link /quote/v2 for AAVE→PEPE cross-chain swap. + * The quote is requested for (inputAmount - feeAmount) to account for the + * pre-bridge fee we take first. + * 2. Parse the approve step to extract the Relay spender address. + * 3. Build either a monolithic or modular execution payload (controlled by + * USE_MODULAR env var). + * 4. Call AllowanceHolder.exec → router.performExecution / performModularExecution. + * + * The script spends the signer’s full on-chain balance of AAVE on Arbitrum as the input amount (fund the wallet and approve AH as needed). + * + * Usage: + * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts + * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts + * + * Notes on the Relay.link quote API: + * - steps[0] is an ERC-20 approve (or absent for native input). + * The approve spender can be decoded from steps[0].items[0].data.data bytes 16..36. + * - steps[1] is the actual deposit call: steps[1].items[0].data.{ to, data }. + * - Relay quotes EXACT_INPUT so the amount in the deposit calldata is already + * correct for the quoted amount; we do NOT splice it at runtime. + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + ROUTER_ADDRESS, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + RELAY_API_KEY, + ALLOWANCE_HOLDER, +} from './config'; +import { execViaAH } from './utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, getWalletErc20Balance } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { + MonolithicExecution, + Action, + CallType, + NO_FEE, + NO_SWAP, + ZERO_ADDRESS, +} from './utils/contractTypes'; + +// ─── Relay.link quote ───────────────────────────────────────────────────────── + +interface RelayStep { + items: Array<{ + data: { + to?: string; + data?: string; + }; + }>; +} + +interface RelayQuoteResponse { + steps: RelayStep[]; +} + +/** + * Fetches a cross-chain quote from Relay.link for AAVE→PEPE. + * + * @param routerAddress The router contract (= the "user" that sends the deposit) + * @param recipient The final recipient on Base (signer's EOA) + * @param amount Net amount after fee, in AAVE wei + */ +async function fetchRelayQuote( + routerAddress: string, + recipient: string, + amount: bigint, +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (RELAY_API_KEY) { + headers['x-api-key'] = RELAY_API_KEY; + } + + const body = { + user: routerAddress, + recipient, + originChainId: CHAIN_IDS.ARBITRUM, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_ARB, + destinationCurrency: TOKENS.AAVE_BASE, + tradeType: 'EXACT_INPUT', + amount: amount.toString(), + }; + + const response = await axios.post( + 'https://api.relay.link/quote/v2', + body, + { headers }, + ); + return response.data; +} + +/** + * Parses the Relay.link quote to extract: + * - relaySpender: the address that needs ERC-20 approval (from approve step calldata) + * - depositTarget: the contract to call with depositData + * - depositData: the calldata for the deposit call + */ +function parseRelayQuote(quote: RelayQuoteResponse): { + relaySpender: string; + depositTarget: string; + depositData: string; +} { + // The approve step's calldata encodes: approve(spender, amount) + // selector (4 bytes) + spender (32 bytes padded) → spender starts at byte 16 of the full hex + const approveStep = quote.steps[0]; + const approveData = approveStep.items[0].data.data ?? ''; + // spender is at bytes [4..36] of the approve calldata (after 4-byte selector) + const relaySpender = ethers.getAddress( + '0x' + approveData.slice(4 + 8 + 24, 4 + 8 + 24 + 40), + ); + + const depositStep = quote.steps[1]; + const depositItem = depositStep.items[0].data; + const depositTarget = depositItem.to ?? ''; + const depositData = depositItem.data ?? '0x'; + + return { relaySpender, depositTarget, depositData }; +} + +// ─── Monolithic builder ─────────────────────────────────────────────────────── + +/** + * Builds a MonolithicExecution that: + * - Pulls inputAmount of AAVE from user via AH + * - Sends feeAmount of AAVE to signer as pre-bridge fee + * - Approves Relay spender for (inputAmount - feeAmount) + * - Calls Relay deposit target with deposit calldata (amount already correct in calldata) + */ +function buildMonolithicExecution( + signerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + relaySpender: string, + depositTarget: string, + depositData: string, +): MonolithicExecution { + return { + input: { + user: signerAddress, + inputToken: TOKENS.AAVE_ARB, + inputAmount, + }, + preFee: { + receiver: signerAddress, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: depositTarget, + approvalSpender: relaySpender, + value: 0n, + data: depositData, + amountPositions: [], // Relay calldata is already for the correct amount + useFinalAmountAsValue: false, + }, + }; +} + +// ─── Modular builder ────────────────────────────────────────────────────────── + +/** + * Builds an Action array that achieves the same flow as monolithic but + * as discrete steps: + * [0] Pull AAVE from user via AH.transferFrom (AH already has allowance) + * [1] Transfer feeAmount AAVE to signer (pre-bridge fee) + * [2] Approve Relay spender for bridgeAmount AAVE + * [3] Call Relay deposit + * + * Note: In the modular path the router calls AH.transferFrom directly as an + * action, since AH has the transient allowance granted by AH.exec() on entry. + * AH.transferFrom selector: 0x15dacbea (address token, address owner, address recipient, uint256 amount) + */ +function buildModularActions( + signerAddress: string, + routerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + bridgeAmount: bigint, + relaySpender: string, + depositTarget: string, + depositData: string, +): Action[] { + // AH.transferFrom(token, owner, recipient, amount) = 0x15dacbea + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.AAVE_ARB, + signerAddress, + routerAddress, + inputAmount, + ]); + + return [ + // 0: pull AAVE from user via AllowanceHolder.transferFrom + { + callType: CallType.CALL, + target: ALLOWANCE_HOLDER, + value: 0n, + data: ahTransferFromData, + splices: [], + }, + // 1: send pre-bridge fee to signer in AAVE + { + callType: CallType.CALL, + target: TOKENS.AAVE_ARB, + value: 0n, + data: encodeTransfer(signerAddress, feeAmount), + splices: [], + }, + // 2: approve Relay spender for bridgeAmount + { + callType: CallType.CALL, + target: TOKENS.AAVE_ARB, + value: 0n, + data: encodeApprove(relaySpender, bridgeAmount), + splices: [], + }, + // 3: call Relay deposit — amount already encoded in depositData + { + callType: CallType.CALL, + target: depositTarget, + value: 0n, + data: depositData, + splices: [], + }, + ]; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_ARB; + const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (inputAmount === 0n) { + throw new Error( + `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with AAVE on Arbitrum first.`, + ); + } + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + const useModular = true; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ADDRESS}`); + console.log(`Input token: ${inputToken}`); + console.log(`Input amount: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`); + console.log( + `Fee amount: ${ethers.formatUnits(feeAmount, inputDecimals)} (${FEE_BPS} bps)`, + ); + console.log(`Bridge amount: ${ethers.formatUnits(bridgeAmount, inputDecimals)}`); + console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); + console.log(''); + + // Fetch Relay.link quote for bridgeAmount + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuote( + ROUTER_ADDRESS, + signerAddress, + bridgeAmount, + ); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + console.log(''); + + const routerIface = new ethers.Interface(ROUTER_ABI); + let execCalldata: string; + + if (useModular) { + const actions = buildModularActions( + signerAddress, + ROUTER_ADDRESS, + inputAmount, + feeAmount, + bridgeAmount, + relaySpender, + depositTarget, + depositData, + ); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + actions, + ]); + console.log('Using performModularExecution'); + } else { + const exec = buildMonolithicExecution( + signerAddress, + inputAmount, + feeAmount, + relaySpender, + depositTarget, + depositData, + ); + execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); + console.log('Using performExecution (monolithic)'); + } + + console.log('Sending AllowanceHolder.exec transaction...'); + const receipt = await execViaAH( + signer, + ROUTER_ADDRESS, // operator + TOKENS.AAVE_ARB, // token to grant allowance for + inputAmount, // amount + ROUTER_ADDRESS, // target (the router) + execCalldata, + ); + + console.log(`\nSuccess! Gas used: ${receipt.gasUsed.toString()}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts new file mode 100644 index 0000000..89332de --- /dev/null +++ b/scripts/e2e/config.ts @@ -0,0 +1,96 @@ +/** + * Shared configuration: addresses, chain IDs, token info, and CCTP config + * used across all e2e scripts. + */ +import * as dotenv from 'dotenv'; +import { MaxUint256 } from 'ethers'; +dotenv.config(); + +// ─── Chain IDs ─────────────────────────────────────────────────────────────── + +export const CHAIN_IDS = { + ETHEREUM: 1, + ARBITRUM: 42161, + BASE: 8453, +} as const; + +// ─── Contract addresses ─────────────────────────────────────────────────────── + +/** 0x AllowanceHolder — same address on every EVM chain */ +export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; + +/** Deployed BungeeOpenRouterV2Unchecked instance (set via env after deployment) */ +export const ROUTER_ADDRESS: string = ''; + +/** Sentinel used in modular actions to forward address(this).balance as msg.value */ +export const MAX_UINT256 = MaxUint256; + +/** Standard ERC-20 "native" sentinel used by CurrencyLib */ +export const NATIVE_TOKEN_ADDRESS = + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + +// ─── Token addresses ────────────────────────────────────────────────────────── + +export const TOKENS = { + AAVE_ARB: '0xba5DdD1f9d7F570dc94a51479a000E3BCE967196', + AAVE_ETH: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', + USDC_ARB: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + USDC_BASE: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + USDC_ETH: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + AAVE_BASE: '0x63706e401c06ac8513145b7687a14804d17f814b', +} as const; + +// ─── CCTP v2 configuration ──────────────────────────────────────────────────── + +export interface CctpChainConfig { + tokenMessenger: string; + /** Circle's domain identifier for CCTP */ + cctpDomain: number; + usdcAddress: string; +} + +export const CCTP_CONFIG: Record = { + [CHAIN_IDS.ARBITRUM]: { + tokenMessenger: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + cctpDomain: 3, + usdcAddress: TOKENS.USDC_ARB, + }, + [CHAIN_IDS.BASE]: { + tokenMessenger: '0x1682Ae6375C4E4A97e4B583BC394c861A46D8962', + cctpDomain: 6, + usdcAddress: TOKENS.USDC_BASE, + }, + [CHAIN_IDS.ETHEREUM]: { + tokenMessenger: '0xBd3fa81B58Ba92a82136038B25aDec7066af3155', + cctpDomain: 0, + usdcAddress: TOKENS.USDC_ETH, + }, +}; + +// ─── Arbitrum bridge ────────────────────────────────────────────────────────── + +/** Arbitrum Delayed Inbox — accepts ETH deposits via depositEth() */ +export const ARBITRUM_INBOX = '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'; + +// ─── Fee config ─────────────────────────────────────────────────────────────── + +/** Fee applied in scripts that take pre-/post-route fees (basis points). */ +export const FEE_BPS = Number(process.env.FEE_AMOUNT_BPS ?? '10'); + +export function bpsOf(amount: bigint, bps: number): bigint { + return (amount * BigInt(bps)) / 10000n; +} + +// ─── RPC endpoints ──────────────────────────────────────────────────────────── + +export const RPC = { + ARBITRUM: process.env.ARBITRUM_RPC ?? 'https://arb1.arbitrum.io/rpc', + ETHEREUM: process.env.ETHEREUM_RPC ?? 'https://eth.llamarpc.com', + BASE: process.env.BASE_RPC ?? 'https://mainnet.base.org', +} as const; + +// ─── API keys ───────────────────────────────────────────────────────────────── + +export const RELAY_API_KEY: string | undefined = process.env.RELAY_API_KEY; +export const OPEN_OCEAN_API_KEY: string | undefined = + process.env.OPEN_OCEAN_API_KEY; diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts new file mode 100644 index 0000000..fcd4e4f --- /dev/null +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -0,0 +1,411 @@ +/** + * Script 3 — Swap AAVE→ETH on Ethereum, then bridge ETH to Arbitrum via + * the Arbitrum native inbox (depositEth) + * + * Flow: + * 1. Fetch an OpenOcean swap quote for AAVE→ETH on Ethereum mainnet. + * 2. Estimate the Arbitrum retryable submission fee using @arbitrum/sdk so we + * know the minimum ETH required to bridge. A conservative fallback of + * 0.001 ETH is used if estimation fails. + * 3. Build a post-swap fee to signer in ETH. + * 4. Build either monolithic or modular execution payload. + * - Monolithic: swap AAVE→ETH (balance delta on NATIVE), take ETH fee, + * call Arbitrum inbox with useFinalAmountAsValue=true so finalAmount + * becomes msg.value on the depositEth call. + * - Modular: pull → approve(oo) → swap(oo) → send ETH fee via low-level call → + * depositEth with value=MAX_UINT256 sentinel (forwards address(this).balance). + * 5. Call AllowanceHolder.exec with msg.value=0 (AAVE is the input token, not ETH). + * + * Uses the signer’s full AAVE balance on Ethereum mainnet as swap input. + * + * Usage: + * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts + * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts + * + * Notes: + * - The router must retain enough ETH after the swap to cover both the fee + * and the Arbitrum retryable submission cost. The script warns if the + * estimated ETH output is insufficient. + * - The Arbitrum Delayed Inbox address is 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f + * on Ethereum mainnet. depositEth() accepts ETH as msg.value and credits + * the sender's L2 address on Arbitrum. + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + ROUTER_ADDRESS, + TOKENS, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, +} from './config'; +import { execViaAH } from './utils/allowanceHolder'; +import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { + MonolithicExecution, + Action, + CallType, + NO_FEE, + ZERO_ADDRESS, + USE_CONTRACT_BALANCE, +} from './utils/contractTypes'; + +// ─── Arbitrum retryable fee estimation ─────────────────────────────────────── + +/** + * Estimates the minimum ETH required for the Arbitrum inbox submission fee. + * Uses @arbitrum/sdk's ParentToChildMessageGasEstimator if available. + * Falls back to a conservative hardcoded estimate (0.001 ETH) so the script + * can run without a live Arbitrum RPC for fee estimation. + * + * For a depositEth(), the inbox contract only needs the submission fee; there + * is no retryable gas limit to estimate (it's a direct ETH credit on L2). + */ +async function estimateArbitrumBridgeFee( + ethereumProvider: ethers.Provider, +): Promise { + try { + // @arbitrum/sdk types vary between versions; dynamic import avoids hard-dep issues. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(ethereumProvider); + // Estimate submission fee for a minimal retryable (0 calldata, 250k gas limit). + const l2GasPrice = + (await ( + await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData() + ).gasPrice) ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee( + ethereumProvider, + 0n, // l1BaseFee (fetched internally) + 0n, // callDataLength + ); + // Add buffer: submission fee + retryable execution cost headroom + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); // gasPrice * 1.2 + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log( + `Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`, + ); + return totalFee; + } catch (err) { + // Fallback: 0.001 ETH is a safe overestimate for L1→L2 ETH deposits in 2024-2026. + const fallback = ethers.parseEther('0.001'); + console.warn( + `Could not estimate Arbitrum fee via SDK (${ + (err as Error).message + }), using fallback: ${ethers.formatEther(fallback)} ETH`, + ); + return fallback; + } +} + +// ─── OpenOcean swap quote ───────────────────────────────────────────────────── + +interface OpenOceanSwapQuoteResponse { + data: { + to: string; + data: string; + value: string; + outAmount: string; + minOutAmount: string; + }; +} + +/** + * Fetches an OpenOcean swap quote for AAVE→ETH on Ethereum mainnet. + * The native ETH output address used by OpenOcean is 0xEeee...EEe. + */ +async function fetchOpenOceanSwapQuote( + routerAddress: string, + inputAmount: bigint, + slippageBps: number = 100, +): Promise<{ + ooRouterAddress: string; + swapData: string; + minAmountOut: bigint; + estimatedOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_ETH, + outTokenAddress: NATIVE_TOKEN_ADDRESS, // ETH output + amount: ethers.formatUnits(inputAmount, 18), + slippage: (slippageBps / 100).toString(), + sender: routerAddress, + account: routerAddress, + gasPrice: '20', + }; + if (OPEN_OCEAN_API_KEY) { + params['apikey'] = OPEN_OCEAN_API_KEY; + } + + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + + return { + ooRouterAddress: q.to, + swapData: q.data, + minAmountOut: BigInt(q.minOutAmount), + estimatedOut: BigInt(q.outAmount), + }; +} + +// ─── Arbitrum inbox calldata ────────────────────────────────────────────────── + +/** + * Builds the calldata for Arbitrum inbox depositEth(). + * The ETH amount is entirely determined by msg.value — there is no amount + * parameter in the calldata itself. + */ +function buildDepositEthCalldata(): string { + const iface = new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]); + return iface.encodeFunctionData('depositEth', []); +} + +// ─── Monolithic builder ─────────────────────────────────────────────────────── + +/** + * Builds a MonolithicExecution that: + * - Pulls inputAmount AAVE from user + * - Swaps AAVE → ETH via OpenOcean (balance delta on NATIVE_TOKEN_ADDRESS) + * - Takes feeAmount ETH as post-swap fee sent to signer + * - Calls Arbitrum inbox depositEth() with finalAmount as msg.value + * (via useFinalAmountAsValue=true — no amount to splice in calldata) + */ +function buildMonolithicExecution( + signerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + minAmountOut: bigint, + ooRouterAddress: string, + swapData: string, +): MonolithicExecution { + return { + input: { + user: signerAddress, + inputToken: TOKENS.AAVE_ETH, + inputAmount, + }, + preFee: NO_FEE, + swap: { + target: ooRouterAddress, + approvalSpender: ooRouterAddress, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + data: swapData, + }, + postFee: { + receiver: signerAddress, + amount: feeAmount, + }, + bridge: { + target: ARBITRUM_INBOX, + approvalSpender: ZERO_ADDRESS, // no ERC-20 approval needed for native + value: 0n, // ignored when useFinalAmountAsValue=true + data: buildDepositEthCalldata(), + amountPositions: [], // ETH goes as msg.value, not in calldata + useFinalAmountAsValue: true, // forward finalAmount as msg.value to inbox + }, + }; +} + +// ─── Modular builder ────────────────────────────────────────────────────────── + +/** + * Builds an Action array: + * [0] Pull AAVE via AH.transferFrom + * [1] Approve OpenOcean router for inputAmount + * [2] Call OpenOcean to swap AAVE → ETH (lands in router as ETH) + * [3] Send ETH fee to signer via low-level call (value=feeAmount) + * [4] Call Arbitrum inbox depositEth() with value=MAX_UINT256 sentinel + * so _dispatchAction forwards address(this).balance (all remaining ETH) + */ +function buildModularActions( + signerAddress: string, + routerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + ooRouterAddress: string, + swapData: string, +): Action[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.AAVE_ETH, + signerAddress, + routerAddress, + inputAmount, + ]); + + return [ + // 0: pull AAVE from user via AllowanceHolder + { + callType: CallType.CALL, + target: ALLOWANCE_HOLDER, + value: 0n, + data: ahTransferFromData, + splices: [], + }, + // 1: approve OpenOcean to spend AAVE + { + callType: CallType.CALL, + target: TOKENS.AAVE_ETH, + value: 0n, + data: encodeApprove(ooRouterAddress, inputAmount), + splices: [], + }, + // 2: swap AAVE → ETH via OpenOcean (ETH lands in the router) + { + callType: CallType.CALL, + target: ooRouterAddress, + value: 0n, + data: swapData, + splices: [], + }, + // 3: send ETH fee to signer — target receives feeAmount as msg.value via CALL + // The signer EOA must be payable; any standard address accepts ETH. + { + callType: CallType.CALL, + target: signerAddress, + value: feeAmount, + data: '0x', // empty calldata = plain ETH transfer + splices: [], + }, + // 4: deposit remaining ETH to Arbitrum via inbox.depositEth() + // USE_CONTRACT_BALANCE sentinel → _dispatchAction substitutes address(this).balance + { + callType: CallType.CALL, + target: ARBITRUM_INBOX, + value: USE_CONTRACT_BALANCE, + data: buildDepositEthCalldata(), + splices: [], + }, + ]; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_ETH; + const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (inputAmount === 0n) { + throw new Error( + `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with AAVE on Ethereum first.`, + ); + } + const useModular = true; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ADDRESS}`); + console.log(`Input token: ${inputToken}`); + console.log( + `Input: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`, + ); + console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); + console.log(''); + + // Fetch OpenOcean quote (AAVE → ETH on Ethereum) + console.log('Fetching OpenOcean swap quote (AAVE→ETH Ethereum)...'); + const { ooRouterAddress, swapData, minAmountOut, estimatedOut } = + await fetchOpenOceanSwapQuote(ROUTER_ADDRESS, inputAmount); + + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(`OO Router: ${ooRouterAddress}`); + console.log(`Est. ETH out: ${ethers.formatEther(estimatedOut)} ETH`); + console.log( + `Post-swap fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`, + ); + console.log(`Min ETH out: ${ethers.formatEther(minAmountOut)} ETH`); + + // Estimate Arbitrum bridge fee + const arbFee = await estimateArbitrumBridgeFee(provider); + const minEthRequired = feeAmount + arbFee; + if (estimatedOut < minEthRequired) { + console.warn( + `Warning: estimated ETH output (${ethers.formatEther( + estimatedOut, + )}) may be insufficient ` + + `to cover fee + bridge cost (${ethers.formatEther( + minEthRequired, + )}). Increase AAVE balance on Ethereum so the quoted swap output rises.`, + ); + } + console.log(''); + + const routerIface = new ethers.Interface(ROUTER_ABI); + let execCalldata: string; + + if (useModular) { + const actions = buildModularActions( + signerAddress, + ROUTER_ADDRESS, + inputAmount, + feeAmount, + ooRouterAddress, + swapData, + ); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + actions, + ]); + console.log('Using performModularExecution'); + } else { + const exec = buildMonolithicExecution( + signerAddress, + inputAmount, + feeAmount, + minAmountOut, + ooRouterAddress, + swapData, + ); + execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); + console.log('Using performExecution (monolithic)'); + } + + // AH.exec is called with AAVE as the token grant — ETH is handled internally + // by the swap. msg.value=0 since the input token is ERC-20. + console.log('Sending AllowanceHolder.exec transaction...'); + const receipt = await execViaAH( + signer, + ROUTER_ADDRESS, + TOKENS.AAVE_ETH, + inputAmount, + ROUTER_ADDRESS, + execCalldata, + 0n, // no ETH needed from caller; ETH comes from the swap output + ); + + console.log(`\nSuccess! Gas used: ${receipt.gasUsed.toString()}`); + console.log( + `ETH will arrive on Arbitrum at ${signerAddress} (via inbox deposit).`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts new file mode 100644 index 0000000..a479788 --- /dev/null +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -0,0 +1,415 @@ +/** + * Script 2 — Swap AAVE→USDC on Arbitrum, then bridge USDC to Base via CCTP v2 + * + * Flow: + * 1. Fetch an OpenOcean swap quote for AAVE→USDC on Arbitrum. + * 2. Build CCTP v2 depositForBurn calldata with a zero amount placeholder + * at byte offset 4 (the first parameter). + * 3. Build either a monolithic or modular execution payload. + * - Monolithic: swap inside the router using pre/post balance delta, + * take a post-swap fee in USDC, splice finalAmount into depositForBurn, + * approve TOKEN_MESSENGER, call TOKEN_MESSENGER. + * - Modular: discrete actions — pull → approve(oo) → swap(oo) → transfer fee → + * approve(cctp) → staticcall balanceOf → call depositForBurn (splice balance→amount). + * 4. Call AllowanceHolder.exec → router.performExecution / performModularExecution. + * + * Uses the signer’s full AAVE balance on Arbitrum as swap input (fund the wallet and approve AH as needed). + * + * Usage: + * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts + * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts + * + * CCTP v2 fast path: + * minFinalityThreshold=1000 (1000 confirmations, ~instant finality on supported chains) + * maxFee set to a small value; pass 0 for the standard (slower) path. + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + ROUTER_ADDRESS, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + ALLOWANCE_HOLDER, +} from './config'; +import { execViaAH } from './utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { + MonolithicExecution, + Action, + CallType, + NO_FEE, + ZERO_ADDRESS, +} from './utils/contractTypes'; + +// ─── OpenOcean swap quote ───────────────────────────────────────────────────── + +interface OpenOceanSwapQuoteResponse { + data: { + to: string; + data: string; + value: string; + estimatedGas: string; + outAmount: string; + minOutAmount: string; + }; +} + +/** + * Fetches a swap quote from OpenOcean for AAVE→USDC on Arbitrum. + * The router address is used as both sender and account so OpenOcean + * routes the swap through the router itself. + * + * @param routerAddress Address that will execute the swap (needs approval) + * @param inputAmount Amount of AAVE in wei + * @param slippageBps Slippage tolerance in basis points (e.g. 100 = 1%) + */ +async function fetchOpenOceanSwapQuote( + routerAddress: string, + inputAmount: bigint, + slippageBps: number = 100, +): Promise<{ + routerAddress: string; + swapData: string; + minAmountOut: bigint; + estimatedOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_ARB, + outTokenAddress: TOKENS.USDC_ARB, + amount: ethers.formatUnits(inputAmount, 18), // OO expects human-readable amount + slippage: (slippageBps / 100).toString(), + sender: routerAddress, + account: routerAddress, + gasPrice: '1', // gwei; doesn't affect routing + }; + if (OPEN_OCEAN_API_KEY) { + params['apikey'] = OPEN_OCEAN_API_KEY; + } + + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + + return { + routerAddress: q.to, + swapData: q.data, + minAmountOut: BigInt(q.minOutAmount), + estimatedOut: BigInt(q.outAmount), + }; +} + +// ─── CCTP depositForBurn calldata ───────────────────────────────────────────── + +/** + * Builds CCTP v2 depositForBurn calldata. + * `amount` is set to 0 as a placeholder; it will be spliced in at runtime + * (offset 4 in the calldata, i.e. amountPositions=[4] in MonolithicExecution). + * + * For the modular path a STATICCALL balanceOf + splice is used instead. + * + * Fast path: minFinalityThreshold=1000, maxFee=small value + * Standard path: minFinalityThreshold=2000, maxFee=0 + */ +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, + fastPath: boolean = true, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + + // Pad the recipient address to bytes32 + const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); + + // maxFee: small fee for fast path (e.g. 1 USDC = 1_000_000 units), 0 for standard + const maxFee = fastPath ? 1_000_000n : 0n; + const minFinalityThreshold = fastPath ? 1000 : 2000; + + return iface.encodeFunctionData('depositForBurn', [ + 0n, // amount placeholder — spliced at runtime + destinationCctpDomain, + mintRecipient, + burnToken, + ethers.ZeroHash, // destinationCaller = anyone can complete + maxFee, + minFinalityThreshold, + ]); +} + +// ─── Monolithic builder ─────────────────────────────────────────────────────── + +/** + * Builds a MonolithicExecution that: + * - Pulls inputAmount AAVE from user + * - No pre-swap fee + * - Swaps AAVE → USDC via OpenOcean (balance delta) + * - Takes feeAmount USDC as post-swap fee to signer + * - Splices finalAmount into depositForBurn at offset 4 + * - Approves TOKEN_MESSENGER and calls depositForBurn + */ +function buildMonolithicExecution( + signerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + minAmountOut: bigint, + ooRouterAddress: string, + swapData: string, + depositForBurnData: string, + tokenMessenger: string, +): MonolithicExecution { + return { + input: { + user: signerAddress, + inputToken: TOKENS.AAVE_ARB, + inputAmount, + }, + preFee: NO_FEE, + swap: { + target: ooRouterAddress, + approvalSpender: ooRouterAddress, + outputToken: TOKENS.USDC_ARB, + value: 0n, + minOutput: minAmountOut, + data: swapData, + }, + postFee: { + receiver: signerAddress, + amount: feeAmount, + }, + bridge: { + target: tokenMessenger, + approvalSpender: tokenMessenger, + value: 0n, + data: depositForBurnData, + // amount is the first ABI param → at byte offset 4 (after 4-byte selector) + amountPositions: [4n], + useFinalAmountAsValue: false, + }, + }; +} + +// ─── Modular builder ────────────────────────────────────────────────────────── + +/** + * Builds an Action array: + * [0] Pull AAVE via AH.transferFrom + * [1] Approve OpenOcean router for inputAmount + * [2] Call OpenOcean router to swap AAVE → USDC + * [3] Transfer feeAmount USDC to signer + * [4] Approve TOKEN_MESSENGER for MaxUint256 (covers any USDC balance) + * [5] STATICCALL USDC.balanceOf(router) → prevReturn = 32-byte balance + * [6] Call TOKEN_MESSENGER.depositForBurn → splice prevReturn[0..32] → data[4..36] + */ +function buildModularActions( + signerAddress: string, + routerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + ooRouterAddress: string, + swapData: string, + depositForBurnData: string, + tokenMessenger: string, +): Action[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.AAVE_ARB, + signerAddress, + routerAddress, + inputAmount, + ]); + + return [ + // 0: pull AAVE from user via AH + { + callType: CallType.CALL, + target: ALLOWANCE_HOLDER, + value: 0n, + data: ahTransferFromData, + splices: [], + }, + // 1: approve OpenOcean to spend AAVE + { + callType: CallType.CALL, + target: TOKENS.AAVE_ARB, + value: 0n, + data: encodeApprove(ooRouterAddress, inputAmount), + splices: [], + }, + // 2: swap AAVE → USDC via OpenOcean + { + callType: CallType.CALL, + target: ooRouterAddress, + value: 0n, + data: swapData, + splices: [], + }, + // 3: send post-swap fee in USDC to signer + { + callType: CallType.CALL, + target: TOKENS.USDC_ARB, + value: 0n, + data: encodeTransfer(signerAddress, feeAmount), + splices: [], + }, + // 4: approve TOKEN_MESSENGER for unlimited USDC (router holds exact balance) + { + callType: CallType.CALL, + target: TOKENS.USDC_ARB, + value: 0n, + data: encodeApprove(tokenMessenger, ethers.MaxUint256), + splices: [], + }, + // 5: staticcall USDC.balanceOf(router) → prevReturn = ABI-encoded uint256 balance + { + callType: CallType.STATICCALL, + target: TOKENS.USDC_ARB, + value: 0n, + data: encodeBalanceOf(routerAddress), + splices: [], + }, + // 6: depositForBurn — splice the 32-byte balance from prevReturn into the + // amount field at dstOffset=4 (first param, after the 4-byte selector) + { + callType: CallType.CALL, + target: tokenMessenger, + value: 0n, + data: depositForBurnData, + splices: [ + { + srcOffset: 0n, // read from start of prevReturn (the ABI uint256) + dstOffset: 4n, // write into depositForBurn calldata after selector + length: 32n, // uint256 = 32 bytes + }, + ], + }, + ]; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_ARB; + const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (inputAmount === 0n) { + throw new Error( + `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with AAVE on Arbitrum first.`, + ); + } + const arbCctp = CCTP_CONFIG[CHAIN_IDS.ARBITRUM]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + const useModular = true; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ADDRESS}`); + console.log(`Input token: ${inputToken}`); + console.log( + `Input: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`, + ); + console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); + console.log(''); + + // Fetch OpenOcean quote + console.log('Fetching OpenOcean swap quote (AAVE→USDC Arbitrum)...'); + const { + routerAddress: ooRouterAddress, + swapData, + minAmountOut, + estimatedOut, + } = await fetchOpenOceanSwapQuote(ROUTER_ADDRESS, inputAmount); + + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(`OO Router: ${ooRouterAddress}`); + console.log(`Est. USDC out: ${ethers.formatUnits(estimatedOut, 6)} USDC`); + console.log( + `Post-swap fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(`Min USDC out: ${ethers.formatUnits(minAmountOut, 6)} USDC`); + console.log(''); + + // Build CCTP depositForBurn calldata (amount=0 placeholder, will be spliced) + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, // recipient on Base + arbCctp.usdcAddress, // token being burned + baseCctp.cctpDomain, // destination domain = Base + true, // fast path + ); + + const routerIface = new ethers.Interface(ROUTER_ABI); + let execCalldata: string; + + if (useModular) { + const actions = buildModularActions( + signerAddress, + ROUTER_ADDRESS, + inputAmount, + feeAmount, + ooRouterAddress, + swapData, + depositForBurnData, + arbCctp.tokenMessenger, + ); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + actions, + ]); + console.log('Using performModularExecution'); + } else { + const exec = buildMonolithicExecution( + signerAddress, + inputAmount, + feeAmount, + minAmountOut, + ooRouterAddress, + swapData, + depositForBurnData, + arbCctp.tokenMessenger, + ); + execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); + console.log('Using performExecution (monolithic)'); + } + + console.log('Sending AllowanceHolder.exec transaction...'); + const receipt = await execViaAH( + signer, + ROUTER_ADDRESS, + TOKENS.AAVE_ARB, + inputAmount, + ROUTER_ADDRESS, + execCalldata, + ); + + console.log(`\nSuccess! Gas used: ${receipt.gasUsed.toString()}`); + console.log( + `USDC will arrive on Base at ${signerAddress} after CCTP attestation.`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/utils/allowanceHolder.ts b/scripts/e2e/utils/allowanceHolder.ts new file mode 100644 index 0000000..2861679 --- /dev/null +++ b/scripts/e2e/utils/allowanceHolder.ts @@ -0,0 +1,65 @@ +/** + * AllowanceHolder helpers. + * + * The AH.exec() flow in a single transaction: + * 1. User calls AllowanceHolder.exec(operator, token, amount, target, data, { value }) + * 2. AH grants transient allowance: operator may pull `amount` of `token` from msg.sender + * 3. AH calls target(data) forwarding msg.value + * 4. Inside target: AllowanceHolder.transferFrom(token, msg.sender_original, recipient, amount) + * pulls the tokens using the transient allowance (cleared after the call) + * + * The router's _pullFromUser uses the same AH.transferFrom to move tokens in. + */ +import { ethers, Signer } from 'ethers'; +import { ALLOWANCE_HOLDER } from '../config'; + +/** + * Minimal ABI fragment for AllowanceHolder — only the exec function we call. + * Full ABI reference: https://docs.0x.org/docs/core-concepts/contracts#allowanceholder-recommended + */ +export const ALLOWANCE_HOLDER_ABI = [ + 'function exec(address operator, address token, uint160 amount, address target, bytes calldata data) external payable returns (bytes memory result)', +] as const; + +/** + * Returns an ethers Contract instance for AllowanceHolder connected to the + * given signer. + */ +export function getAllowanceHolderContract(signer: Signer): ethers.Contract { + return new ethers.Contract(ALLOWANCE_HOLDER, ALLOWANCE_HOLDER_ABI, signer); +} + +/** + * Builds and sends an AllowanceHolder.exec() transaction. + * + * @param signer - The EOA signing and paying for the tx (= the "user") + * @param operator - The contract that will pull funds (our router) + * @param token - ERC-20 token to grant ephemeral allowance for + * @param amount - Exact amount the operator is allowed to pull + * @param target - Contract to call after granting the allowance (our router) + * @param callData - Encoded function call on `target` + * @param txValue - Optional ETH to forward with the call (for native-token flows) + */ +export async function execViaAH( + signer: Signer, + operator: string, + token: string, + amount: bigint, + target: string, + callData: string, + txValue?: bigint, +): Promise { + const ah = getAllowanceHolderContract(signer); + + const tx = await ah.exec(operator, token, amount, target, callData, { + value: txValue ?? 0n, + }); + + console.log(`AllowanceHolder.exec tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error(`Transaction failed: ${tx.hash}`); + } + console.log(`Transaction confirmed in block ${receipt.blockNumber}`); + return receipt; +} diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts new file mode 100644 index 0000000..bc33e92 --- /dev/null +++ b/scripts/e2e/utils/contractTypes.ts @@ -0,0 +1,87 @@ +/** + * TypeScript interfaces that mirror every Solidity struct in + * BungeeOpenRouterV2Unchecked. The order and field names must match the ABI + * produced by the compiler so that ethers.js can encode them correctly. + */ + +// ─── Monolithic execution types ─────────────────────────────────────────────── + +export interface InputData { + user: string; + inputToken: string; + inputAmount: bigint; +} + +export interface FeeData { + receiver: string; + amount: bigint; +} + +export interface SwapData { + target: string; + approvalSpender: string; + outputToken: string; + value: bigint; + minOutput: bigint; + data: string; +} + +export interface BridgeData { + target: string; + approvalSpender: string; + value: bigint; + data: string; + amountPositions: bigint[]; + useFinalAmountAsValue: boolean; +} + +export interface MonolithicExecution { + input: InputData; + preFee: FeeData; + swap: SwapData; + postFee: FeeData; + bridge: BridgeData; +} + +// ─── Modular execution types ────────────────────────────────────────────────── + +export enum CallType { + CALL = 0, + DELEGATECALL = 1, + STATICCALL = 2, +} + +export interface Splice { + srcOffset: bigint; + dstOffset: bigint; + length: bigint; +} + +export interface Action { + callType: CallType; + target: string; + /** Use MAX_UINT256 sentinel to forward address(this).balance */ + value: bigint; + data: string; + splices: Splice[]; +} + +// ─── Sentinel / zero helpers ────────────────────────────────────────────────── + +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +/** Sentinel value: _dispatchAction forwards address(this).balance as msg.value */ +export const USE_CONTRACT_BALANCE = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); + +/** Convenience: empty fee (no fee taken) */ +export const NO_FEE: FeeData = { receiver: ZERO_ADDRESS, amount: 0n }; + +/** Convenience: empty swap (skip swap step) */ +export const NO_SWAP: SwapData = { + target: ZERO_ADDRESS, + approvalSpender: ZERO_ADDRESS, + outputToken: ZERO_ADDRESS, + value: 0n, + minOutput: 0n, + data: '0x', +}; diff --git a/scripts/e2e/utils/erc20.ts b/scripts/e2e/utils/erc20.ts new file mode 100644 index 0000000..7716911 --- /dev/null +++ b/scripts/e2e/utils/erc20.ts @@ -0,0 +1,87 @@ +/** + * ERC-20 calldata encoding helpers used when building modular Action arrays. + * These produce raw encoded bytes rather than making any actual calls, so they + * work for both building action.data fields and for direct contract calls. + */ +import { ethers } from 'ethers'; + +const ERC20_IFACE = new ethers.Interface([ + 'function approve(address spender, uint256 amount) external returns (bool)', + 'function transfer(address recipient, uint256 amount) external returns (bool)', + 'function balanceOf(address account) external view returns (uint256)', + 'function allowance(address owner, address spender) external view returns (uint256)', +]); + +/** + * Encodes ERC-20 approve(spender, amount) calldata. + */ +export function encodeApprove(spender: string, amount: bigint): string { + return ERC20_IFACE.encodeFunctionData('approve', [spender, amount]); +} + +/** + * Encodes ERC-20 transfer(recipient, amount) calldata. + */ +export function encodeTransfer(recipient: string, amount: bigint): string { + return ERC20_IFACE.encodeFunctionData('transfer', [recipient, amount]); +} + +/** + * Encodes ERC-20 balanceOf(account) calldata for use in a STATICCALL action. + * The return value is a 32-byte ABI-encoded uint256 — can be spliced directly + * into a subsequent action's calldata. + */ +export function encodeBalanceOf(account: string): string { + return ERC20_IFACE.encodeFunctionData('balanceOf', [account]); +} + +/** + * Returns an ethers Contract instance for a standard ERC-20 token (read-only). + * Pass a provider or signer as the second argument. + */ +export function getErc20Contract(tokenAddress: string, providerOrSigner: ethers.Provider | ethers.Signer): ethers.Contract { + return new ethers.Contract( + tokenAddress, + [ + 'function approve(address spender, uint256 amount) external returns (bool)', + 'function allowance(address owner, address spender) external view returns (uint256)', + 'function balanceOf(address account) external view returns (uint256)', + 'function decimals() external view returns (uint8)', + ], + providerOrSigner, + ); +} + +/** + * Reads an ERC-20 balance and decimals for `owner`, similar to bungee-sandbox + * `getTokenBalance` patterns (fund the signer, then swap/bridge whole balance). + */ +export async function getWalletErc20Balance( + tokenAddress: string, + owner: string, + provider: ethers.Provider, +): Promise<{ balance: bigint; decimals: number }> { + const token = getErc20Contract(tokenAddress, provider); + const [balanceRaw, decimalsRaw] = await Promise.all([ + token.balanceOf(owner), + token.decimals(), + ]); + const balance = typeof balanceRaw === 'bigint' ? balanceRaw : BigInt(balanceRaw.toString()); + + return { balance, decimals: Number(decimalsRaw) }; +} + +/** + * Convenience: approve a spender with a real provider transaction. + */ +export async function approveErc20( + tokenAddress: string, + spender: string, + amount: bigint, + signer: ethers.Signer, +): Promise { + const token = getErc20Contract(tokenAddress, signer); + const tx = await token.approve(spender, amount); + await tx.wait(); + console.log(`Approved ${spender} for ${amount} of ${tokenAddress}`); +} diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts new file mode 100644 index 0000000..5ed4e64 --- /dev/null +++ b/scripts/e2e/utils/routerAbi.ts @@ -0,0 +1,21 @@ +/** + * ABI fragment for BungeeOpenRouterV2Unchecked — only the two entrypoints + * called from e2e scripts. Structs must exactly match the Solidity definitions. + */ +export const ROUTER_ABI = [ + // Monolithic path + `function performExecution( + ( + (address user, address inputToken, uint256 inputAmount) input, + (address receiver, uint256 amount) preFee, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, bytes data) swap, + (address receiver, uint256 amount) postFee, + (address target, address approvalSpender, uint256 value, bytes data, uint256[] amountPositions, bool useFinalAmountAsValue) bridge + ) exec + ) external payable`, + + // Modular path + `function performModularExecution( + (uint8 callType, address target, uint256 value, bytes data, (uint256 srcOffset, uint256 dstOffset, uint256 length)[] splices)[] actions + ) external payable`, +] as const; From 9a86303d188ab00cdbd2433ee4ed547426bbad93 Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 12 May 2026 14:10:59 +0400 Subject: [PATCH 10/69] feat: js util for generic executor --- utils/openRouterExecutionBuilder.d.ts | 84 ++++++ utils/openRouterExecutionBuilder.js | 354 ++++++++++++++++++++++++++ utils/openRouterExecutionBuilder.md | 88 +++++++ 3 files changed, 526 insertions(+) create mode 100644 utils/openRouterExecutionBuilder.d.ts create mode 100644 utils/openRouterExecutionBuilder.js create mode 100644 utils/openRouterExecutionBuilder.md diff --git a/utils/openRouterExecutionBuilder.d.ts b/utils/openRouterExecutionBuilder.d.ts new file mode 100644 index 0000000..87b772b --- /dev/null +++ b/utils/openRouterExecutionBuilder.d.ts @@ -0,0 +1,84 @@ +export type Hex = `0x${string}`; +export type Address = Hex; +export type BigNumberish = bigint | number | string; + +export interface ExecutionContext { + [key: string]: unknown; +} + +export interface ReturnSource { + sourceActionIndex: number; + srcOffset: number; + length: number; +} + +export interface Splice { + sourceActionIndex: BigNumberish; + srcOffset: BigNumberish; + dstOffset: BigNumberish; + length: BigNumberish; +} + +export interface Action { + callType: number; + target: Address; + data: Hex; + splices: Splice[]; +} + +export declare const DUMMY_ROUTER_EXECUTE_SELECTOR: "0x8749f339"; + +export declare const CallType: Readonly<{ + CALL: 0; + STATICCALL: 1; + CALL_WITH_NATIVE: 2; +}>; + +export declare const Offset: Readonly<{ + selectorArg(argIndex: BigNumberish): number; + nativePayload(payloadOffset: BigNumberish): number; +}>; + +export declare class OpenRouterExecution { + context: ExecutionContext; + constructor(context?: ExecutionContext); + call(target: Address, data: Hex): ActionHandle; + staticCall(target: Address, data: Hex): ActionHandle; + callWithNative(target: Address, payload?: Hex, value?: BigNumberish): ActionHandle; + nativeCall(target: Address, payload?: Hex, value?: BigNumberish): ActionHandle; + action(action: { callType: BigNumberish; target: Address; data?: Hex; splices?: Splice[] }): ActionHandle; + ref(labelOrIndex: string | BigNumberish): ActionRef; + actionAt(index: BigNumberish): Action; + toActions(): Action[]; + toJSON(): unknown; + toDummyRouterCalldata(): Hex; +} + +export declare class ActionHandle { + readonly execution: OpenRouterExecution; + readonly index: number; + as(label: string): this; + label(label: string): this; + ref(): ActionRef; + return(offset?: BigNumberish, length?: BigNumberish): ReturnSource; + returnWord(offset?: BigNumberish): ReturnSource; + splice(source: ReturnSource, dstOffset: BigNumberish, length?: BigNumberish): this; + spliceWord(dstOffset: BigNumberish, source: ReturnSource): this; + spliceArg(argIndex: BigNumberish, source: ReturnSource): this; + spliceNativeValue(source: ReturnSource): this; + valueFrom(source: ReturnSource): this; + splicePayloadWord(payloadOffset: BigNumberish, source: ReturnSource): this; + splicePayload(payloadOffset: BigNumberish, source: ReturnSource, length?: BigNumberish): this; + patchWord(dstOffset: BigNumberish, source: ReturnSource): this; +} + +export declare class ActionRef { + readonly index: number; + readonly label?: string; + return(srcOffset?: BigNumberish, length?: BigNumberish): ReturnSource; + returnWord(srcOffset?: BigNumberish): ReturnSource; +} + +export declare function concatHex(values: Hex[]): Hex; +export declare function encodeDummyRouterExecuteArgs(actions: Action[]): Hex; +export declare function encodeWord(value: BigNumberish): Hex; diff --git a/utils/openRouterExecutionBuilder.js b/utils/openRouterExecutionBuilder.js new file mode 100644 index 0000000..16746d4 --- /dev/null +++ b/utils/openRouterExecutionBuilder.js @@ -0,0 +1,354 @@ +"use strict"; + +const DUMMY_ROUTER_EXECUTE_SELECTOR = "0x8749f339"; +const WORD_BYTES = 32; +const WORD_HEX_CHARS = WORD_BYTES * 2; +const UINT256_MAX = (1n << 256n) - 1n; + +const CallType = Object.freeze({ + CALL: 0, + STATICCALL: 1, + CALL_WITH_NATIVE: 2, +}); + +const Offset = Object.freeze({ + selectorArg: (argIndex) => 4 + WORD_BYTES * checkedIndex(argIndex, "argIndex"), + nativePayload: (payloadOffset) => WORD_BYTES + checkedIndex(payloadOffset, "payloadOffset"), +}); + +class OpenRouterExecution { + constructor(context = {}) { + this.context = { ...context }; + this._actions = []; + this._labels = new Map(); + } + + call(target, data) { + return this.action({ callType: CallType.CALL, target, data }); + } + + staticCall(target, data) { + return this.action({ callType: CallType.STATICCALL, target, data }); + } + + callWithNative(target, payload = "0x", value = 0n) { + return this.action({ + callType: CallType.CALL_WITH_NATIVE, + target, + data: concatHex([encodeWord(value), payload]), + }); + } + + nativeCall(target, payload = "0x", value = 0n) { + return this.callWithNative(target, payload, value); + } + + action({ callType, target, data = "0x", splices = [] }) { + const actionIndex = this._actions.length; + const action = { + callType: checkedCallType(callType), + target: normalizeAddress(target), + data: normalizeHex(data, "data"), + splices: splices.map((splice, index) => normalizeSplice(splice, `splices[${index}]`)), + }; + for (const splice of action.splices) { + validateSpliceForAction(actionIndex, action, splice); + } + this._actions.push(action); + return new ActionHandle(this, this._actions.length - 1); + } + + ref(labelOrIndex) { + if (typeof labelOrIndex === "string") { + if (!this._labels.has(labelOrIndex)) { + throw new Error(`Unknown action label: ${labelOrIndex}`); + } + return new ActionRef(this._labels.get(labelOrIndex), labelOrIndex); + } + return new ActionRef(checkedIndex(labelOrIndex, "actionIndex")); + } + + actionAt(index) { + const checked = checkedIndex(index, "actionIndex"); + const action = this._actions[checked]; + if (!action) throw new Error(`Unknown action index: ${checked}`); + return action; + } + + toActions() { + return this._actions.map(cloneAction); + } + + toJSON() { + return this._actions.map((action) => ({ + callType: action.callType, + target: action.target, + data: action.data, + splices: action.splices.map((splice) => ({ + sourceActionIndex: String(splice.sourceActionIndex), + srcOffset: String(splice.srcOffset), + dstOffset: String(splice.dstOffset), + length: String(splice.length), + })), + })); + } + + toDummyRouterCalldata() { + return concatHex([DUMMY_ROUTER_EXECUTE_SELECTOR, encodeDummyRouterExecuteArgs(this._actions)]); + } + + _label(index, label) { + if (!label || typeof label !== "string") throw new Error("Action label must be a non-empty string"); + if (this._labels.has(label)) throw new Error(`Duplicate action label: ${label}`); + this._labels.set(label, index); + return new ActionRef(index, label); + } + + _splice(index, splice) { + const action = this.actionAt(index); + const normalized = normalizeSplice(splice, "splice"); + validateSpliceForAction(index, action, normalized); + action.splices.push(normalized); + } +} + +class ActionHandle { + constructor(execution, index) { + this.execution = execution; + this.index = index; + } + + as(label) { + this.execution._label(this.index, label); + return this; + } + + label(label) { + return this.as(label); + } + + ref() { + return new ActionRef(this.index); + } + + return(offset = 0, length = WORD_BYTES) { + return this.ref().return(offset, length); + } + + returnWord(offset = 0) { + return this.ref().returnWord(offset); + } + + splice(source, dstOffset, length = source.length) { + this.execution._splice(this.index, { + sourceActionIndex: source.sourceActionIndex, + srcOffset: source.srcOffset, + dstOffset, + length, + }); + return this; + } + + spliceWord(dstOffset, source) { + return this.splice(source, dstOffset, WORD_BYTES); + } + + spliceArg(argIndex, source) { + return this.spliceWord(Offset.selectorArg(argIndex), source); + } + + spliceNativeValue(source) { + return this.spliceWord(0, source); + } + + valueFrom(source) { + return this.spliceNativeValue(source); + } + + splicePayloadWord(payloadOffset, source) { + return this.spliceWord(Offset.nativePayload(payloadOffset), source); + } + + splicePayload(payloadOffset, source, length = source.length) { + return this.splice(source, Offset.nativePayload(payloadOffset), length); + } + + patchWord(dstOffset, source) { + return this.spliceWord(dstOffset, source); + } +} + +class ActionRef { + constructor(index, label) { + this.index = index; + this.label = label; + } + + return(srcOffset = 0, length = WORD_BYTES) { + return { + sourceActionIndex: this.index, + srcOffset: checkedIndex(srcOffset, "srcOffset"), + length: checkedIndex(length, "length"), + }; + } + + returnWord(srcOffset = 0) { + return this.return(srcOffset, WORD_BYTES); + } +} + +function encodeDummyRouterExecuteArgs(actions) { + return concatHex([encodeWord(WORD_BYTES), encodeActionArray(actions)]); +} + +function encodeActionArray(actions) { + const encodedActions = actions.map(encodeActionTuple); + let nextOffset = WORD_BYTES * actions.length; + const offsets = []; + for (const encodedAction of encodedActions) { + offsets.push(encodeWord(nextOffset)); + nextOffset += hexByteLength(encodedAction); + } + return concatHex([encodeWord(actions.length), ...offsets, ...encodedActions]); +} + +function encodeActionTuple(action) { + const encodedData = encodeBytes(action.data); + const encodedSplices = encodeSpliceArray(action.splices); + const dataOffset = WORD_BYTES * 4; + const splicesOffset = dataOffset + hexByteLength(encodedData); + + return concatHex([ + encodeWord(action.callType), + encodeAddressWord(action.target), + encodeWord(dataOffset), + encodeWord(splicesOffset), + encodedData, + encodedSplices, + ]); +} + +function encodeSpliceArray(splices) { + const encodedSplices = splices.flatMap((splice) => [ + encodeWord(splice.sourceActionIndex), + encodeWord(splice.srcOffset), + encodeWord(splice.dstOffset), + encodeWord(splice.length), + ]); + return concatHex([encodeWord(splices.length), ...encodedSplices]); +} + +function encodeBytes(value) { + const hex = strip0x(normalizeHex(value, "bytes")); + const byteLength = hex.length / 2; + const paddedLength = Math.ceil(byteLength / WORD_BYTES) * WORD_HEX_CHARS; + return `0x${strip0x(encodeWord(byteLength))}${hex.padEnd(paddedLength, "0")}`; +} + +function encodeAddressWord(value) { + return `0x${strip0x(normalizeAddress(value)).padStart(WORD_HEX_CHARS, "0")}`; +} + +function encodeWord(value) { + const bigint = toBigInt(value); + if (bigint < 0n || bigint > UINT256_MAX) throw new Error(`uint256 out of range: ${value}`); + return `0x${bigint.toString(16).padStart(WORD_HEX_CHARS, "0")}`; +} + +function concatHex(values) { + return `0x${values.map((value) => strip0x(normalizeHex(value, "hex"))).join("")}`; +} + +function normalizeSplice(splice, label) { + if (!splice || typeof splice !== "object") throw new Error(`${label} must be an object`); + const length = checkedIndex(splice.length, `${label}.length`); + if (length === 0) throw new Error(`${label}.length must be greater than zero`); + return { + sourceActionIndex: checkedIndex(splice.sourceActionIndex, `${label}.sourceActionIndex`), + srcOffset: checkedIndex(splice.srcOffset, `${label}.srcOffset`), + dstOffset: checkedIndex(splice.dstOffset, `${label}.dstOffset`), + length, + }; +} + +function validateSpliceForAction(actionIndex, action, splice) { + if (splice.sourceActionIndex >= actionIndex) { + throw new Error(`Invalid future splice: action ${actionIndex} cannot read action ${splice.sourceActionIndex}`); + } + if (splice.dstOffset + splice.length > hexByteLength(action.data)) { + throw new Error( + `Splice destination exceeds action ${actionIndex} data length: ${splice.dstOffset} + ${splice.length}`, + ); + } +} + +function checkedCallType(callType) { + const value = checkedIndex(callType, "callType"); + if (![CallType.CALL, CallType.STATICCALL, CallType.CALL_WITH_NATIVE].includes(value)) { + throw new Error(`Unsupported callType: ${callType}`); + } + return value; +} + +function checkedIndex(value, label) { + const bigint = toBigInt(value); + if (bigint < 0n || bigint > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`${label} must fit in a safe non-negative integer`); + } + return Number(bigint); +} + +function toBigInt(value) { + if (typeof value === "bigint") return value; + if (typeof value === "number") { + if (!Number.isInteger(value)) throw new Error(`Expected integer, got ${value}`); + return BigInt(value); + } + if (typeof value === "string") { + if (value.startsWith("0x") || value.startsWith("0X")) return BigInt(value); + return BigInt(value); + } + throw new Error(`Expected bigint, number, or numeric string, got ${typeof value}`); +} + +function normalizeAddress(value) { + const hex = strip0x(normalizeHex(value, "address")); + if (hex.length !== 40) throw new Error(`Invalid address length: ${value}`); + return `0x${hex.toLowerCase()}`; +} + +function normalizeHex(value, label) { + if (typeof value !== "string") throw new Error(`${label} must be a hex string`); + if (!/^0x[0-9a-fA-F]*$/.test(value)) throw new Error(`${label} must be 0x-prefixed hex`); + if (value.length % 2 !== 0) throw new Error(`${label} must contain whole bytes`); + return value.toLowerCase(); +} + +function strip0x(value) { + return value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value; +} + +function hexByteLength(value) { + return strip0x(normalizeHex(value, "hex")).length / 2; +} + +function cloneAction(action) { + return { + callType: action.callType, + target: action.target, + data: action.data, + splices: action.splices.map((splice) => ({ ...splice })), + }; +} + +module.exports = { + ActionHandle, + ActionRef, + CallType, + DUMMY_ROUTER_EXECUTE_SELECTOR, + Offset, + OpenRouterExecution, + concatHex, + encodeDummyRouterExecuteArgs, + encodeWord, +}; diff --git a/utils/openRouterExecutionBuilder.md b/utils/openRouterExecutionBuilder.md new file mode 100644 index 0000000..2670973 --- /dev/null +++ b/utils/openRouterExecutionBuilder.md @@ -0,0 +1,88 @@ +# OpenRouter Execution Builder + +Dependency-free helper for formatting `DummyRouter.execute(Action[])` payloads from provider SDK/API calldata. + +```js +const { OpenRouterExecution } = require("./openRouterExecutionBuilder"); + +const exec = new OpenRouterExecution({ + routeId: "openocean-stargate-native", + chainId: 42161, +}); + +exec.call(USDC, approveCalldata).as("approve"); +exec.call(OPENOCEAN_EXCHANGE_V2, openOceanSwapCalldata).as("swap"); + +exec + .staticCall(MATH_MANIPULATOR, percentCalldataWithZeroAmount) + .as("routeFee") + .spliceArg(0, exec.ref("swap").returnWord()); + +exec + .nativeCall(FEE_RECIPIENT) + .as("feeTransfer") + .valueFrom(exec.ref("routeFee").returnWord()); + +const calldata = exec.toDummyRouterCalldata(); +``` + +## Offset Helpers + +- `spliceArg(argIndex, source)` writes a 32-byte source into a normal ABI calldata argument. It maps `argIndex` to `4 + argIndex * 32`. +- `valueFrom(source)` writes a 32-byte source into the leading value word used by `CALL_WITH_NATIVE`. +- `splicePayloadWord(payloadOffset, source)` writes into the payload of a `CALL_WITH_NATIVE`. It automatically adds the 32-byte value prefix. +- `splicePayload(payloadOffset, source, length)` does the same for non-word slices. +- `patchWord(dstOffset, source)` writes directly to an absolute calldata offset. + +## Across Shape + +```js +exec.call(ARBITRUM_USDC, approveOpenOcean).as("approve"); +exec.call(OPENOCEAN_EXCHANGE_V2, openOceanSwapCalldata).as("swap"); + +exec + .staticCall(ACROSS_AMOUNT_MANIPULATOR, deriveOutputAmountWithZeroInput) + .as("acrossOutputAmount") + .spliceArg(0, exec.ref("swap").returnWord()); + +exec.call(ARBITRUM_WETH, approveAcross).as("approveAcross"); + +exec + .call(ACROSS_SPOKE_POOL, acrossDepositWithZeroAmounts) + .as("acrossDeposit") + .patchWord(132, exec.ref("swap").returnWord()) + .patchWord(164, exec.ref("acrossOutputAmount").returnWord()); +``` + +## Stargate Native Shape + +```js +exec.call(ARBITRUM_USDC, approveOpenOcean).as("approve"); +exec.call(OPENOCEAN_EXCHANGE_V2, openOceanSwapCalldata).as("swap"); + +exec + .staticCall(MATH_MANIPULATOR, percentCalldataWithZeroAmount) + .as("routeFee") + .spliceArg(0, exec.ref("swap").returnWord()); + +exec.nativeCall(FEE_RECIPIENT).as("feeTransfer").valueFrom(exec.ref("routeFee").returnWord()); + +exec + .staticCall(MATH_MANIPULATOR, subtractWithZeroArgs) + .as("postFeeAmount") + .spliceArg(0, exec.ref("swap").returnWord()) + .spliceArg(1, exec.ref("routeFee").returnWord()); + +exec + .staticCall(MATH_MANIPULATOR, subtractNativeFeeFromZeroAmount) + .as("bridgeAmount") + .spliceArg(0, exec.ref("postFeeAmount").returnWord()); + +exec + .nativeCall(STARGATE_NATIVE_WRAPPER, stargateSendCalldata) + .as("stargate") + .valueFrom(exec.ref("postFeeAmount").returnWord()) + .splicePayloadWord(STARGATE_AMOUNT_OFFSET, exec.ref("bridgeAmount").returnWord()); +``` + +Use `toActions()` when the caller already has an ABI encoder. Use `toDummyRouterCalldata()` when you need raw calldata for the current `DummyRouter`. From 78319abf7b46032d258b7e7e4819390c2af79f67 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 15:49:01 +0530 Subject: [PATCH 11/69] fix: ah calldata, approval --- .gitignore | 9 +++- scripts/e2e/bridgeViaRelay.ts | 27 ++++++++---- scripts/e2e/config.ts | 2 +- scripts/e2e/swapBridgeViaArbitrumNative.ts | 3 +- scripts/e2e/swapBridgeViaCctp.ts | 3 +- scripts/e2e/utils/allowanceHolder.ts | 49 +++++++++++++++++++++- 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 1c14cdb..e18c785 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,11 @@ docs/ .env -node_modules \ No newline at end of file +node_modules + +/typechain +/artifacts +/cache_hardhat +/cache-hh +/artifacts-tron +/cache_hardhat-tron \ No newline at end of file diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index 7f19c45..f937844 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -114,14 +114,27 @@ function parseRelayQuote(quote: RelayQuoteResponse): { depositTarget: string; depositData: string; } { - // The approve step's calldata encodes: approve(spender, amount) - // selector (4 bytes) + spender (32 bytes padded) → spender starts at byte 16 of the full hex + const approveIface = new ethers.Interface([ + 'function approve(address spender, uint256 amount) external returns (bool)', + ]); + const approveStep = quote.steps[0]; - const approveData = approveStep.items[0].data.data ?? ''; - // spender is at bytes [4..36] of the approve calldata (after 4-byte selector) - const relaySpender = ethers.getAddress( - '0x' + approveData.slice(4 + 8 + 24, 4 + 8 + 24 + 40), - ); + const approveDataHex = approveStep.items[0].data.data ?? ''; + let relaySpender: string; + try { + relaySpender = ethers.getAddress( + approveIface.decodeFunctionData('approve', approveDataHex)[0], + ); + } catch { + /** Some routes use Permit2 signatures instead of naked approve; spender may still appear in abi.encode-like layout. Fallback: last 20 bytes of first argument word. */ + const normalized = approveDataHex.startsWith('0x') ? approveDataHex.slice(2) : approveDataHex; + if (normalized.length < 8 + 64) { + throw new Error('Relay approve step calldata too short for fallback spender parse'); + } + const spender40 = normalized.slice(8 + 24, 8 + 24 + 40); + + relaySpender = ethers.getAddress('0x' + spender40); + } const depositStep = quote.steps[1]; const depositItem = depositStep.items[0].data; diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 89332de..748570e 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -20,7 +20,7 @@ export const CHAIN_IDS = { export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; /** Deployed BungeeOpenRouterV2Unchecked instance (set via env after deployment) */ -export const ROUTER_ADDRESS: string = ''; +export const ROUTER_ADDRESS: string = '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648'; /** Sentinel used in modular actions to forward address(this).balance as msg.value */ export const MAX_UINT256 = MaxUint256; diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index fcd4e4f..44d4fc8 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -47,7 +47,7 @@ import { ALLOWANCE_HOLDER, NATIVE_TOKEN_ADDRESS, } from './config'; -import { execViaAH } from './utils/allowanceHolder'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; import { @@ -388,6 +388,7 @@ async function main() { // AH.exec is called with AAVE as the token grant — ETH is handled internally // by the swap. msg.value=0 since the input token is ERC-20. + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec transaction...'); const receipt = await execViaAH( signer, diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index a479788..2bb998c 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -39,7 +39,7 @@ import { OPEN_OCEAN_API_KEY, ALLOWANCE_HOLDER, } from './config'; -import { execViaAH } from './utils/allowanceHolder'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; import { @@ -393,6 +393,7 @@ async function main() { console.log('Using performExecution (monolithic)'); } + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec transaction...'); const receipt = await execViaAH( signer, diff --git a/scripts/e2e/utils/allowanceHolder.ts b/scripts/e2e/utils/allowanceHolder.ts index 2861679..6b986a4 100644 --- a/scripts/e2e/utils/allowanceHolder.ts +++ b/scripts/e2e/utils/allowanceHolder.ts @@ -10,15 +10,23 @@ * * The router's _pullFromUser uses the same AH.transferFrom to move tokens in. */ -import { ethers, Signer } from 'ethers'; +import { ethers, MaxUint256, Signer } from 'ethers'; import { ALLOWANCE_HOLDER } from '../config'; +import { getErc20Contract } from './erc20'; /** * Minimal ABI fragment for AllowanceHolder — only the exec function we call. + * + * IMPORTANT: the amount parameter MUST be uint256 (not uint160). AllowanceHolder + * is entirely implemented in its fallback() function, which dispatches by comparing + * the incoming 4-byte selector against IAllowanceHolder.exec.selector. That selector + * is keccak256("exec(address,address,uint256,address,bytes)")[0:4] = 0x2213bc0b. + * Using uint160 would produce a different selector and the call would revert. + * * Full ABI reference: https://docs.0x.org/docs/core-concepts/contracts#allowanceholder-recommended */ export const ALLOWANCE_HOLDER_ABI = [ - 'function exec(address operator, address token, uint160 amount, address target, bytes calldata data) external payable returns (bytes memory result)', + 'function exec(address operator, address token, uint256 amount, address target, bytes calldata data) external payable returns (bytes memory result)', ] as const; /** @@ -29,6 +37,43 @@ export function getAllowanceHolderContract(signer: Signer): ethers.Contract { return new ethers.Contract(ALLOWANCE_HOLDER, ALLOWANCE_HOLDER_ABI, signer); } +/** + * Reads `token.allowance(owner, AllowanceHolder)` and, if it is below + * `requiredAmount`, submits `approve(AllowanceHolder, MaxUint256)`. + * + * AH.exec pulls FROM the user's wallet via ephemeral allowance keyed by operator; + * the user must have a persistent ERC20 approval to AH first. + */ +export async function ensureAllowanceForAllowanceHolder( + signer: Signer, + token: string, + requiredAmount: bigint, +): Promise { + const owner = await signer.getAddress(); + const erc20 = getErc20Contract(token, signer); + const allowanceRaw = await erc20.allowance(owner, ALLOWANCE_HOLDER); + const allowance = + typeof allowanceRaw === 'bigint' ? allowanceRaw : BigInt(allowanceRaw.toString()); + + if (allowance >= requiredAmount) { + console.log( + `AllowanceHolder allowance OK (${allowance.toString()} >= ${requiredAmount.toString()})`, + ); + return; + } + + console.log( + `Approving AllowanceHolder: allowance ${allowance.toString()} < required ${requiredAmount.toString()}, sending approve...`, + ); + const tx = await erc20.approve(ALLOWANCE_HOLDER, MaxUint256); + console.log(`approve tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error(`ERC20 approve to AllowanceHolder failed: ${tx.hash}`); + } + console.log('AllowanceHolder approval confirmed'); +} + /** * Builds and sends an AllowanceHolder.exec() transaction. * From 90ca9d3a27da18c4a5208d293a7fcbc8365857ed Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 16:48:16 +0530 Subject: [PATCH 12/69] fix: scripts --- scripts/e2e/bridgeViaRelay.ts | 2 +- scripts/e2e/config.ts | 4 ++-- scripts/e2e/swapBridgeViaCctp.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index f937844..0b62e24 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -281,7 +281,7 @@ async function main() { const feeAmount = bpsOf(inputAmount, FEE_BPS); const bridgeAmount = inputAmount - feeAmount; - const useModular = true; + const useModular = false; console.log(`Signer: ${signerAddress}`); console.log(`Router: ${ROUTER_ADDRESS}`); diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 748570e..6b1afbb 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -20,7 +20,7 @@ export const CHAIN_IDS = { export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; /** Deployed BungeeOpenRouterV2Unchecked instance (set via env after deployment) */ -export const ROUTER_ADDRESS: string = '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648'; +export const ROUTER_ADDRESS: string = '0x98381Fb4dC5c2046558236857181F4e34a9088dC'; /** Sentinel used in modular actions to forward address(this).balance as msg.value */ export const MAX_UINT256 = MaxUint256; @@ -51,7 +51,7 @@ export interface CctpChainConfig { export const CCTP_CONFIG: Record = { [CHAIN_IDS.ARBITRUM]: { - tokenMessenger: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d', cctpDomain: 3, usdcAddress: TOKENS.USDC_ARB, }, diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index 2bb998c..c103efd 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -323,7 +323,7 @@ async function main() { } const arbCctp = CCTP_CONFIG[CHAIN_IDS.ARBITRUM]; const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; - const useModular = true; + const useModular = false; console.log(`Signer: ${signerAddress}`); console.log(`Router: ${ROUTER_ADDRESS}`); From 39d5750a348f5226964dec15a84b7c5c38c6a8d4 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 16:53:48 +0530 Subject: [PATCH 13/69] fix: AH _pullFromUser assembly --- src/combined/BungeeOpenRouterV2.sol | 7 +++++-- src/combined/BungeeOpenRouterV2Unchecked.sol | 7 +++++-- src/monolithic/BungeeOpenRouterAH.sol | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol index daa550d..bd3e5e9 100644 --- a/src/combined/BungeeOpenRouterV2.sol +++ b/src/combined/BungeeOpenRouterV2.sol @@ -299,8 +299,11 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { let ptr := mload(0x40) mstore(add(0x80, ptr), amount) mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears recipient padding - mstore(add(0x2c, ptr), shl(0xa0, token)) // clears owner padding + mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding + // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which + // shifts the 20-byte address out of place and corrupts the calldata token. Same as + // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. + mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index dca08f5..d0483d5 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -268,8 +268,11 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { let ptr := mload(0x40) mstore(add(0x80, ptr), amount) mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears recipient padding - mstore(add(0x2c, ptr), shl(0xa0, token)) // clears owner padding + mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding + // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which + // shifts the 20-byte address out of place and corrupts the calldata token. Same as + // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. + mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { diff --git a/src/monolithic/BungeeOpenRouterAH.sol b/src/monolithic/BungeeOpenRouterAH.sol index 3340b53..2f0f560 100644 --- a/src/monolithic/BungeeOpenRouterAH.sol +++ b/src/monolithic/BungeeOpenRouterAH.sol @@ -52,8 +52,11 @@ contract BungeeOpenRouterAH is BungeeOpenRouter, AllowanceHolderContext { let ptr := mload(0x40) mstore(add(0x80, ptr), amount) mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears recipient padding - mstore(add(0x2c, ptr), shl(0xa0, token)) // clears owner padding + mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding + // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which + // shifts the 20-byte address out of place and corrupts the calldata token. Same as + // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. + mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { From c01aed7ca8395ce33c2e680945c6fefd2f7dc02d Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 12 May 2026 19:51:55 +0530 Subject: [PATCH 14/69] feat: stargate native test --- scripts/e2e/config.ts | 19 + scripts/e2e/swapBridgeViaStargateNative.ts | 527 +++++++++++++++++++++ 2 files changed, 546 insertions(+) create mode 100644 scripts/e2e/swapBridgeViaStargateNative.ts diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 6b1afbb..9b74376 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -72,6 +72,25 @@ export const CCTP_CONFIG: Record = { /** Arbitrum Delayed Inbox — accepts ETH deposits via depositEth() */ export const ARBITRUM_INBOX = '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'; +// ─── Stargate Native Pool (ETH Arbitrum → ETH Base) ───────────────────────── + +/** + * Stargate Native ETH OFT adapter on Arbitrum. + * Call send() with msg.value = amountLD + nativeFee to bridge ETH to Base. + * Ref: poc-openrouter/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol + */ +export const STARGATE_NATIVE_ARB = '0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F'; + +/** LayerZero v2 endpoint ID for Base (EID 30184). Used in Stargate sendParam.dstEid. */ +export const BASE_LZ_EID = 30184; + +/** + * Byte offset of sendParam.amountLD within the Stargate send() calldata (after the 4-byte selector). + * Layout: selector(4) + head[sendParam_ptr(32) + nativeFee(32) + lzTokenFee(32) + refundAddr(32)] + + * tail[dstEid(32) + to(32)] + amountLD = 4+128+32+32 = 196 + */ +export const STARGATE_AMOUNT_LD_OFFSET = 196; + // ─── Fee config ─────────────────────────────────────────────────────────────── /** Fee applied in scripts that take pre-/post-route fees (basis points). */ diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts new file mode 100644 index 0000000..f25e50e --- /dev/null +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -0,0 +1,527 @@ +/** + * Script 4 — Swap USDC Arbitrum → native ETH Arbitrum via OpenOcean, + * then bridge ETH Arbitrum → ETH Base via Stargate Native Pool + * + * Flow: + * 1. Fetch the signer's full USDC balance on Arbitrum as input. + * 2. Fetch an OpenOcean swap_quote for USDC → native ETH on Arbitrum. + * 3. Call Stargate quoteSend to get the LayerZero nativeFee. + * 4. Pre-compute amountLD = estimatedFinalAmount - nativeFee for the Stargate calldata. + * 5. Build either a monolithic or modular execution payload: + * - Monolithic: pull USDC via AH → swap USDC→ETH → post-fee (ETH to signer) → + * Stargate send (useFinalAmountAsValue=true, static amountLD) + * - Modular: pull USDC → approve OO → swap → ETH fee transfer → + * Stargate send (value=USE_CONTRACT_BALANCE, static amountLD) + * 6. Ensure AllowanceHolder ERC20 allowance for USDC. + * 7. Execute AllowanceHolder.exec, forwarding nativeFee as msg.value so the + * router has enough ETH to cover both the bridge amount and the LZ fee. + * + * Stargate Native Pool design notes: + * Stargate's send() requires msg.value = amountLD + nativeFee. We pre-encode + * amountLD = estimatedFinalAmount - nativeFee in the calldata (static — no splice). + * The caller provides nativeFee as msg.value to AH.exec; this is forwarded to the + * router alongside the USDC. After the swap and fee deduction, the router's ETH + * balance = actualFinalAmount + nativeFee, which is always >= amountLD + nativeFee + * as long as the actual swap output >= the OO minimum (guaranteed by slippage). + * Any excess ETH is refunded to the signer by Stargate via refundAddress. + * + * Usage: + * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts + * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + ROUTER_ADDRESS, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from './config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; +import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { + MonolithicExecution, + Action, + CallType, + NO_FEE, + ZERO_ADDRESS, +} from './utils/contractTypes'; + +// ─── Stargate ABI ───────────────────────────────────────────────────────────── + +/** + * Minimal Stargate OFT/Native pool ABI fragments needed for quoting and bridging. + * The SendParam struct and MessagingFee struct are encoded inline as tuples. + */ +const STARGATE_ABI = [ + // Quote the LayerZero fee for a given SendParam + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + // Query OFT limits and expected receive amounts (for informational logging) + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + // Execute the bridge transfer; msg.value = amountLD + nativeFee + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; + +// ─── OpenOcean swap quote ───────────────────────────────────────────────────── + +interface OpenOceanSwapQuoteResponse { + data: { + to: string; + data: string; + value: string; + estimatedGas: string; + outAmount: string; + minOutAmount: string; + }; +} + +/** + * Fetches a swap_quote from OpenOcean for USDC → native ETH on Arbitrum. + * The router is used as both sender and account so the swap output lands in the router. + * + * @param routerAddress Address that will execute the swap (receives ETH output) + * @param inputAmount Amount of USDC in base units (6 decimals) + * @param slippageBps Slippage tolerance in basis points (e.g. 100 = 1%) + */ +async function fetchOpenOceanSwapQuote( + routerAddress: string, + inputAmount: bigint, + slippageBps: number = 100, +): Promise<{ + routerAddress: string; + swapData: string; + minAmountOut: bigint; + estimatedOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + // OpenOcean uses the canonical ETH sentinel for native output + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), // USDC has 6 decimals + slippage: (slippageBps / 100).toString(), + // sender = account = router so the swap executes from and into the router + sender: routerAddress, + account: routerAddress, + gasPrice: '1', // gwei; does not affect routing + }; + if (OPEN_OCEAN_API_KEY) { + params['apikey'] = OPEN_OCEAN_API_KEY; + } + + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + + return { + routerAddress: q.to, + swapData: q.data, + minAmountOut: BigInt(q.minOutAmount), + estimatedOut: BigInt(q.outAmount), + }; +} + +// ─── Stargate quote ─────────────────────────────────────────────────────────── + +/** + * Builds the Stargate SendParam and fetches both quoteSend (nativeFee) and + * quoteOFT (expected receive amount on Base) in parallel. + * + * @param provider JSON-RPC provider connected to Arbitrum + * @param recipientAddress Recipient address on Base (refundAddress for excess) + * @param bridgeAmountLD Tentative amountLD for the quote (wei) + */ +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + recipientAddress: string, + bridgeAmountLD: bigint, +): Promise<{ + nativeFee: bigint; + amountReceivedLD: bigint; +}> { + const stargate = new ethers.Contract(STARGATE_NATIVE_ARB, STARGATE_ABI, provider); + + // Stargate uses bytes32-padded address for `to` + const recipientBytes32 = ethers.zeroPadValue(recipientAddress, 32); + + const sendParam = { + dstEid: BASE_LZ_EID, + to: recipientBytes32, + amountLD: bridgeAmountLD, + // minAmountLD for quoting: use 0 so the quote always succeeds + minAmountLD: 0n, + extraOptions: '0x', // Stargate native pools use empty extraOptions + composeMsg: '0x', + oftCmd: '0x', + }; + + const [messagingFee, oftQuote] = await Promise.all([ + stargate.quoteSend(sendParam, false), // payInLzToken=false → native fee + stargate.quoteOFT(sendParam), + ]); + + return { + nativeFee: messagingFee.nativeFee as bigint, + amountReceivedLD: (oftQuote.oftReceipt.amountReceivedLD as bigint), + }; +} + +// ─── Stargate send() calldata ───────────────────────────────────────────────── + +/** + * Encodes the Stargate send() calldata. + * + * amountLD is the exact amount passed to Stargate. Stargate's Native pool + * requires msg.value = amountLD + nativeFee; the caller must forward the total. + * + * @param amountLD Amount of ETH to bridge (wei); static — no splice needed + * @param nativeFee LayerZero messaging fee (wei) + * @param recipientAddress Recipient on Base (also the refundAddress for excess ETH) + */ +function buildStargateCalldata( + amountLD: bigint, + nativeFee: bigint, + recipientAddress: string, +): string { + const stargateIface = new ethers.Interface(STARGATE_ABI); + const recipientBytes32 = ethers.zeroPadValue(recipientAddress, 32); + + return stargateIface.encodeFunctionData('send', [ + { + dstEid: BASE_LZ_EID, + to: recipientBytes32, + amountLD, + minAmountLD: 0n, // accept any amount received on destination (e2e testing) + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }, + { + nativeFee, + lzTokenFee: 0n, + }, + recipientAddress, // refundAddress: excess ETH (if amountLD < msg.value - nativeFee) goes here + ]); +} + +// ─── Monolithic builder ─────────────────────────────────────────────────────── + +/** + * Builds a MonolithicExecution struct for: + * pull USDC → swap USDC→ETH (OO) → post-fee ETH to signer → Stargate send + * + * Bridge design: + * - amountLD is pre-encoded in stargateData (= estimatedFinalAmount - nativeFee). + * - useFinalAmountAsValue=true forwards the actual post-fee ETH as msg.value. + * - The caller provides nativeFee as msg.value in the AH.exec call so the router's + * ETH balance at bridge time = actualFinalAmount + nativeFee, satisfying + * msg.value >= amountLD + nativeFee. + * - Any excess ETH refunded to the signer by Stargate via refundAddress. + * + * @param signerAddress Signer/recipient address + * @param inputAmount USDC amount in base units + * @param feeAmount Post-swap fee in wei (ETH) + * @param minAmountOut Minimum ETH from swap (wei); swap reverts if output < this + * @param ooRouterAddress OpenOcean router address returned by the quote + * @param swapData OpenOcean swap calldata + * @param stargateData Pre-built Stargate send() calldata + */ +function buildMonolithicExecution( + signerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + minAmountOut: bigint, + ooRouterAddress: string, + swapData: string, + stargateData: string, +): MonolithicExecution { + return { + input: { + user: signerAddress, + inputToken: TOKENS.USDC_ARB, + inputAmount, + }, + preFee: NO_FEE, + swap: { + target: ooRouterAddress, + approvalSpender: ooRouterAddress, + outputToken: NATIVE_TOKEN_ADDRESS, // ETH out + value: 0n, + minOutput: minAmountOut, + data: swapData, + }, + postFee: { + receiver: signerAddress, + amount: feeAmount, + }, + bridge: { + target: STARGATE_NATIVE_ARB, + approvalSpender: ZERO_ADDRESS, // native ETH — no ERC20 approval needed + value: 0n, + data: stargateData, + // amountLD is pre-encoded in stargateData; no runtime splice needed + amountPositions: [], + // Router forwards actualFinalAmount as msg.value to Stargate. + // Caller ensures nativeFee is included in AH.exec msg.value so that + // router's ETH balance = actualFinalAmount + nativeFee at bridge time. + useFinalAmountAsValue: true, + }, + }; +} + +// ─── Modular builder ────────────────────────────────────────────────────────── + +/** + * Builds the Action array for modular execution: + * [0] AH.transferFrom USDC (pull from user) + * [1] USDC.approve(ooRouter, inputAmount) + * [2] Call OO router to swap USDC → ETH + * [3] Send feeAmount ETH to signer (plain ETH transfer) + * [4] Stargate send() with value=MaxUint256 (USE_CONTRACT_BALANCE sentinel) + * + * The USE_CONTRACT_BALANCE sentinel causes _dispatchAction to substitute + * address(this).balance as msg.value. After action[3], the router holds + * (actualFinalAmount + nativeFee) ETH — satisfying Stargate's requirement. + * amountLD in the calldata is pre-encoded and does not need splicing. + * + * @param signerAddress Signer/recipient address + * @param routerAddress Router contract address (receives ETH from swap) + * @param inputAmount USDC input amount + * @param feeAmount Post-swap fee in wei (ETH) + * @param ooRouterAddress OpenOcean router address + * @param swapData OpenOcean swap calldata + * @param stargateData Pre-built Stargate send() calldata + */ +function buildModularActions( + signerAddress: string, + routerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + ooRouterAddress: string, + swapData: string, + stargateData: string, +): Action[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.USDC_ARB, + signerAddress, + routerAddress, + inputAmount, + ]); + + return [ + // 0: pull USDC from user via AllowanceHolder + { + callType: CallType.CALL, + target: ALLOWANCE_HOLDER, + value: 0n, + data: ahTransferFromData, + splices: [], + }, + // 1: approve OpenOcean to spend USDC + { + callType: CallType.CALL, + target: TOKENS.USDC_ARB, + value: 0n, + data: encodeApprove(ooRouterAddress, inputAmount), + splices: [], + }, + // 2: swap USDC → native ETH via OpenOcean (ETH lands in router) + { + callType: CallType.CALL, + target: ooRouterAddress, + value: 0n, + data: swapData, + splices: [], + }, + // 3: send post-swap fee in ETH to signer (plain ETH transfer) + { + callType: CallType.CALL, + target: signerAddress, + value: feeAmount, + data: '0x', + splices: [], + }, + // 4: bridge ETH via Stargate Native Pool + // value=MaxUint256 → _dispatchAction substitutes address(this).balance. + // After action[3] the router has (actualFinalAmount + nativeFee) ETH, + // which satisfies Stargate's msg.value >= amountLD + nativeFee constraint. + { + callType: CallType.CALL, + target: STARGATE_NATIVE_ARB, + value: ethers.MaxUint256, // USE_CONTRACT_BALANCE sentinel + data: stargateData, + splices: [], + }, + ]; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const useModular = true; + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + // ── 1. Read full USDC balance ─────────────────────────────────────────────── + const inputToken = TOKENS.USDC_ARB; + const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (inputAmount === 0n) { + throw new Error( + `Signer ${signerAddress} has zero USDC balance on Arbitrum. ` + + 'Fund the wallet with USDC on Arbitrum first.', + ); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ADDRESS}`); + console.log(`Input token: ${inputToken} (USDC Arbitrum)`); + console.log(`Input: ${ethers.formatUnits(inputAmount, inputDecimals)} USDC (full wallet balance)`); + console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); + console.log(''); + + // ── 2. Fetch OpenOcean swap quote ────────────────────────────────────────── + console.log('Fetching OpenOcean swap quote (USDC → native ETH, Arbitrum)...'); + const { + routerAddress: ooRouterAddress, + swapData, + minAmountOut, + estimatedOut, + } = await fetchOpenOceanSwapQuote(ROUTER_ADDRESS, inputAmount); + + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedFinalAmount = estimatedOut - feeAmount; + + console.log(`OO Router: ${ooRouterAddress}`); + console.log(`Est. ETH out: ${ethers.formatEther(estimatedOut)} ETH`); + console.log(`Post-swap fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(`Est. final amount: ${ethers.formatEther(estimatedFinalAmount)} ETH`); + console.log(`Min ETH out (OO): ${ethers.formatEther(minAmountOut)} ETH`); + console.log(''); + + // ── 3. Fetch Stargate quote (nativeFee + expected receive amount) ─────────── + console.log('Fetching Stargate quoteSend (ETH Arbitrum → ETH Base)...'); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + signerAddress, // recipient on Base + estimatedFinalAmount, // tentative amount for quoting + ); + + // Add 5% buffer to nativeFee to guard against LZ fee fluctuations between + // quote time and tx inclusion (mirrors the EVM_NATIVE_FEE_BUFFER_PERCENT pattern + // in oft.service.ts). + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + + // amountLD to encode in the send() calldata. + // Pre-encoded as (estimatedFinalAmount - nativeFeeWithBuffer) so that + // Stargate's msg.value check (msg.value >= amountLD + nativeFee) passes even + // at the worst-case estimated output. Any excess (actual > estimated) refunds + // to the signer via Stargate's refundAddress mechanism. + const amountLD = estimatedFinalAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) { + throw new Error( + `Estimated final ETH amount (${ethers.formatEther(estimatedFinalAmount)}) ` + + `is too small to cover the Stargate nativeFee (${ethers.formatEther(nativeFeeWithBuffer)}). ` + + 'Increase your USDC balance.', + ); + } + + console.log(`Stargate nativeFee: ${ethers.formatEther(nativeFee)} ETH`); + console.log(`nativeFee (+5% buf): ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(`amountLD (encoded): ${ethers.formatEther(amountLD)} ETH`); + console.log(`Est. received Base: ${ethers.formatEther(amountReceivedLD)} ETH`); + console.log(''); + + // ── 4. Build Stargate send() calldata ────────────────────────────────────── + const stargateData = buildStargateCalldata(amountLD, nativeFeeWithBuffer, signerAddress); + + // ── 5. Build router execution calldata ───────────────────────────────────── + const routerIface = new ethers.Interface(ROUTER_ABI); + let execCalldata: string; + + if (useModular) { + const actions = buildModularActions( + signerAddress, + ROUTER_ADDRESS, + inputAmount, + feeAmount, + ooRouterAddress, + swapData, + stargateData, + ); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); + console.log('Using performModularExecution (modular)'); + } else { + const exec = buildMonolithicExecution( + signerAddress, + inputAmount, + feeAmount, + minAmountOut, + ooRouterAddress, + swapData, + stargateData, + ); + execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); + console.log('Using performExecution (monolithic)'); + } + + // ── 6. Ensure AllowanceHolder ERC20 approval ─────────────────────────────── + // USDC must be approved to AllowanceHolder before the exec call. + // Native ETH (the bridge token) does not require ERC20 approval. + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + // ── 7. Execute via AllowanceHolder.exec ──────────────────────────────────── + // msg.value = nativeFeeWithBuffer (forwarded to the router alongside USDC pull). + // The router needs this ETH in its balance so that after the swap: + // router.balance = actualFinalAmount + nativeFeeWithBuffer + // Stargate: msg.value = actualFinalAmount >= amountLD + nativeFee ✓ + console.log( + `Sending AllowanceHolder.exec with msg.value = ${ethers.formatEther(nativeFeeWithBuffer)} ETH (LZ fee)...`, + ); + const receipt = await execViaAH( + signer, + ROUTER_ADDRESS, // operator — the contract allowed to pull USDC via AH.transferFrom + TOKENS.USDC_ARB, // token AH is granting ephemeral allowance for + inputAmount, // amount of USDC allowed + ROUTER_ADDRESS, // target — the router to call with execCalldata + execCalldata, // encoded performExecution / performModularExecution call + nativeFeeWithBuffer, // msg.value forwarded through AH to cover the LZ nativeFee + ); + + console.log(''); + console.log(`Transaction hash: ${receipt.hash}`); + console.log(`Gas used: ${receipt.gasUsed?.toString() ?? 'unknown'}`); + console.log(''); + console.log('Swap and bridge submitted successfully.'); + console.log(` Swapped: USDC Arbitrum → ETH Arbitrum (~${ethers.formatEther(estimatedOut)} ETH)`); + console.log(` Fee: ~${ethers.formatEther(feeAmount)} ETH to ${signerAddress}`); + console.log(` Bridging: ~${ethers.formatEther(amountLD)} ETH → ETH Base via Stargate`); + console.log(` Recipient on Base: ${signerAddress}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From 5aba9988eafafb58754e55bca6ddd563403a5093 Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 12 May 2026 18:44:23 +0400 Subject: [PATCH 15/69] feat: gas golf --- src/dummyRouter.sol | 54 +++---- test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 51 +++++-- ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 76 ++++++---- utils/openRouterExecutionBuilder.d.ts | 31 +++- utils/openRouterExecutionBuilder.js | 133 +++++++++++++++--- utils/openRouterExecutionBuilder.md | 16 ++- 6 files changed, 266 insertions(+), 95 deletions(-) diff --git a/src/dummyRouter.sol b/src/dummyRouter.sol index 715914e..9218668 100644 --- a/src/dummyRouter.sol +++ b/src/dummyRouter.sol @@ -8,18 +8,10 @@ contract DummyRouter { CALL_WITH_NATIVE } - struct Splice { - uint256 sourceActionIndex; // which previous return data to read from - uint256 srcOffset; // offset inside previous returndata - uint256 dstOffset; // offset inside current calldata - uint256 length; // bytes to copy - } - struct Action { - CallType callType; - address target; + uint256 actionInfo; bytes data; - Splice[] splices; + uint256[] splices; } error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); @@ -38,13 +30,13 @@ contract DummyRouter { // Patch this action's calldata using earlier action results. uint256 splicesLength = action.splices.length; for (uint256 j; j < splicesLength;) { - Splice calldata s = action.splices[j]; - uint256 sourceActionIndex = s.sourceActionIndex; + uint256 spliceInfo = action.splices[j]; + uint256 sourceActionIndex = uint64(spliceInfo); if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); - uint256 length = s.length; - uint256 srcOffset = s.srcOffset; - uint256 dstOffset = s.dstOffset; + uint256 srcOffset = uint64(spliceInfo >> 64); + uint256 dstOffset = uint64(spliceInfo >> 128); + uint256 length = spliceInfo >> 192; bytes memory source = results[sourceActionIndex]; if (srcOffset + length > source.length || dstOffset + length > callData.length) { revert SpliceOutOfBounds(i, j); @@ -60,33 +52,41 @@ contract DummyRouter { } bool success; - bytes memory ret; - CallType callType = action.callType; - address target = action.target; + uint256 actionInfo = action.actionInfo; + bool storeResult = (actionInfo & 0xff00) != 0; + uint256 callType = actionInfo & 0xff; + address target = address(uint160(actionInfo >> 16)); - if (callType == CallType.STATICCALL) { - (success, ret) = target.staticcall(callData); - } else if (callType == CallType.CALL_WITH_NATIVE) { + if (callType == uint256(CallType.STATICCALL)) { + assembly ("memory-safe") { + success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) + } + } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { if (callData.length < 32) revert MissingNativeValue(i); uint256 callValue; uint256 payloadLength = callData.length - 32; assembly ("memory-safe") { callValue := mload(add(callData, 0x20)) success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) + } + } else { + assembly ("memory-safe") { + success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) + } + } + if (!success || storeResult) { + bytes memory ret; + assembly ("memory-safe") { let returnDataSize := returndatasize() ret := mload(0x40) mstore(ret, returnDataSize) returndatacopy(add(ret, 0x20), 0, returnDataSize) mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) } - } else { - (success, ret) = target.call(callData); + if (!success) revert CallFailed(i, ret); + results[i] = ret; } - - if (!success) revert CallFailed(i, ret); - - results[i] = ret; unchecked { ++i; } diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 5ffd2fa..43606d4 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -24,6 +24,7 @@ interface ISpokePool { ) external payable; } // ref tx 0xc0ba134856d0151eebfeb67aabe0eb12db248974f4d78b9d358a6d46dcaa9700 + contract OpenOceanAcrossOpenRouterPoCTest is Test { address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; address internal constant ACROSS_ARBITRUM_SPOKE_POOL = 0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A; @@ -65,7 +66,10 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { _buildActions(manipulator, inputAmount, bridgeFee, vm.parseBytes(OPENOCEAN_SWAP_CALLDATA)); uint256 spokePoolWethBefore = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL); + uint256 gasBeforeExecute = gasleft(); bytes[] memory results = router.execute(actions); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("router.execute gas used", executeGasUsed); _assertPocResult(router, bridgeFee, spokePoolWethBefore, results[2]); } @@ -81,14 +85,14 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { DummyRouter.CallType.CALL, ARBITRUM_USDC, abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), - new DummyRouter.Splice[](0) + new uint256[](0), + false ); - actions[1] = - _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new DummyRouter.Splice[](0)); + actions[1] = _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); - DummyRouter.Splice[] memory outputAmountSplices = new DummyRouter.Splice[](1); - outputAmountSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); + uint256[] memory outputAmountSplices = new uint256[](1); + outputAmountSplices[0] = _splice(1, 0, 4, 32); actions[2] = _action( DummyRouter.CallType.STATICCALL, @@ -96,22 +100,24 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { abi.encodeCall( AcrossERC20AmountManipulator.deriveOutputAmount, (uint256(0), bridgeFee, uint256(18), uint256(18)) ), - outputAmountSplices + outputAmountSplices, + true ); actions[3] = _action( DummyRouter.CallType.CALL, ARBITRUM_WETH, abi.encodeWithSelector(ERC20.approve.selector, ACROSS_ARBITRUM_SPOKE_POOL, type(uint256).max), - new DummyRouter.Splice[](0) + new uint256[](0), + false ); - DummyRouter.Splice[] memory depositSplices = new DummyRouter.Splice[](2); - depositSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 132, length: 32}); - depositSplices[1] = DummyRouter.Splice({sourceActionIndex: 2, srcOffset: 0, dstOffset: 164, length: 32}); + uint256[] memory depositSplices = new uint256[](2); + depositSplices[0] = _splice(1, 0, 132, 32); + depositSplices[1] = _splice(2, 0, 164, 32); actions[4] = _action( - DummyRouter.CallType.CALL, ACROSS_ARBITRUM_SPOKE_POOL, _emptyAcrossDepositCalldata(), depositSplices + DummyRouter.CallType.CALL, ACROSS_ARBITRUM_SPOKE_POOL, _emptyAcrossDepositCalldata(), depositSplices, false ); } @@ -158,9 +164,28 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { DummyRouter.CallType callType, address target, bytes memory data, - DummyRouter.Splice[] memory splices + uint256[] memory splices, + bool storeResult ) internal pure returns (DummyRouter.Action memory) { - return DummyRouter.Action({callType: callType, target: target, data: data, splices: splices}); + return + DummyRouter.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + } + + function _actionInfo(DummyRouter.CallType callType, address target, bool storeResult) + internal + pure + returns (uint256) + { + return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); + } + + function _splice(uint64 sourceActionIndex, uint64 srcOffset, uint64 dstOffset, uint64 length) + internal + pure + returns (uint256) + { + return uint256(sourceActionIndex) | (uint256(srcOffset) << 64) | (uint256(dstOffset) << 128) + | (uint256(length) << 192); } function _toBytes32(address addr) internal pure returns (bytes32) { diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index 3139f44..60b140a 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -208,59 +208,62 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { DummyRouter.CallType.CALL, ARBITRUM_USDC, abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), - new DummyRouter.Splice[](0) + new uint256[](0), + false ); - actions[1] = - _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new DummyRouter.Splice[](0)); + actions[1] = _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); - DummyRouter.Splice[] memory feeSplices = new DummyRouter.Splice[](1); - feeSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); + uint256[] memory feeSplices = new uint256[](1); + feeSplices[0] = _splice(1, 0, 4, 32); actions[2] = _action( DummyRouter.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.percent, (uint256(0), ROUTE_FEE_BPS)), - feeSplices + feeSplices, + true ); - DummyRouter.Splice[] memory feeTransferSplices = new DummyRouter.Splice[](1); - feeTransferSplices[0] = DummyRouter.Splice({sourceActionIndex: 2, srcOffset: 0, dstOffset: 0, length: 32}); + uint256[] memory feeTransferSplices = new uint256[](1); + feeTransferSplices[0] = _splice(2, 0, 0, 32); actions[3] = _action( - DummyRouter.CallType.CALL_WITH_NATIVE, FEE_RECIPIENT, abi.encodePacked(uint256(0)), feeTransferSplices + DummyRouter.CallType.CALL_WITH_NATIVE, + FEE_RECIPIENT, + abi.encodePacked(uint256(0)), + feeTransferSplices, + false ); - DummyRouter.Splice[] memory postFeeSplices = new DummyRouter.Splice[](2); - postFeeSplices[0] = DummyRouter.Splice({sourceActionIndex: 1, srcOffset: 0, dstOffset: 4, length: 32}); - postFeeSplices[1] = DummyRouter.Splice({sourceActionIndex: 2, srcOffset: 0, dstOffset: 36, length: 32}); + uint256[] memory postFeeSplices = new uint256[](2); + postFeeSplices[0] = _splice(1, 0, 4, 32); + postFeeSplices[1] = _splice(2, 0, 36, 32); actions[4] = _action( DummyRouter.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.subtract, (uint256(0), uint256(0))), - postFeeSplices + postFeeSplices, + true ); - DummyRouter.Splice[] memory bridgeAmountSplices = new DummyRouter.Splice[](1); - bridgeAmountSplices[0] = DummyRouter.Splice({sourceActionIndex: 4, srcOffset: 0, dstOffset: 4, length: 32}); + uint256[] memory bridgeAmountSplices = new uint256[](1); + bridgeAmountSplices[0] = _splice(4, 0, 4, 32); actions[5] = _action( DummyRouter.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.subtract, (uint256(0), nativeFee)), - bridgeAmountSplices + bridgeAmountSplices, + true ); - DummyRouter.Splice[] memory stargateSplices = new DummyRouter.Splice[](2); - stargateSplices[0] = DummyRouter.Splice({sourceActionIndex: 4, srcOffset: 0, dstOffset: 0, length: 32}); - stargateSplices[1] = DummyRouter.Splice({ - sourceActionIndex: 5, - srcOffset: 0, - dstOffset: CALL_WITH_NATIVE_PAYLOAD_OFFSET + STARGATE_AMOUNT_OFFSET, - length: 32 - }); + uint256[] memory stargateSplices = new uint256[](2); + stargateSplices[0] = _splice(4, 0, 0, 32); + stargateSplices[1] = _splice(5, 0, uint64(CALL_WITH_NATIVE_PAYLOAD_OFFSET + STARGATE_AMOUNT_OFFSET), 32); actions[6] = _action( DummyRouter.CallType.CALL_WITH_NATIVE, STARGATE_NATIVE_WRAPPER, abi.encodePacked(uint256(0), stargateCalldata), - stargateSplices + stargateSplices, + false ); } @@ -300,9 +303,28 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { DummyRouter.CallType callType, address target, bytes memory data, - DummyRouter.Splice[] memory splices + uint256[] memory splices, + bool storeResult ) internal pure returns (DummyRouter.Action memory) { - return DummyRouter.Action({callType: callType, target: target, data: data, splices: splices}); + return + DummyRouter.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + } + + function _actionInfo(DummyRouter.CallType callType, address target, bool storeResult) + internal + pure + returns (uint256) + { + return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); + } + + function _splice(uint64 sourceActionIndex, uint64 srcOffset, uint64 dstOffset, uint64 length) + internal + pure + returns (uint256) + { + return uint256(sourceActionIndex) | (uint256(srcOffset) << 64) | (uint256(dstOffset) << 128) + | (uint256(length) << 192); } function _toBytes32(address addr) internal pure returns (bytes32) { diff --git a/utils/openRouterExecutionBuilder.d.ts b/utils/openRouterExecutionBuilder.d.ts index 87b772b..3c7d141 100644 --- a/utils/openRouterExecutionBuilder.d.ts +++ b/utils/openRouterExecutionBuilder.d.ts @@ -19,14 +19,23 @@ export interface Splice { length: BigNumberish; } -export interface Action { +export interface LogicalAction { callType: number; target: Address; data: Hex; + storeResult: boolean; splices: Splice[]; } -export declare const DUMMY_ROUTER_EXECUTE_SELECTOR: "0x8749f339"; +export interface DummyRouterAction { + actionInfo: BigNumberish; + data: Hex; + splices: BigNumberish[]; +} + +export type Action = LogicalAction; + +export declare const DUMMY_ROUTER_EXECUTE_SELECTOR: "0xd405eacd"; export declare const CallType: Readonly<{ CALL: 0; @@ -46,10 +55,17 @@ export declare class OpenRouterExecution { staticCall(target: Address, data: Hex): ActionHandle; callWithNative(target: Address, payload?: Hex, value?: BigNumberish): ActionHandle; nativeCall(target: Address, payload?: Hex, value?: BigNumberish): ActionHandle; - action(action: { callType: BigNumberish; target: Address; data?: Hex; splices?: Splice[] }): ActionHandle; + action(action: { + callType: BigNumberish; + target: Address; + data?: Hex; + splices?: Splice[]; + storeResult?: boolean; + }): ActionHandle; ref(labelOrIndex: string | BigNumberish): ActionRef; - actionAt(index: BigNumberish): Action; - toActions(): Action[]; + actionAt(index: BigNumberish): LogicalAction; + toActions(): DummyRouterAction[]; + toLogicalActions(): LogicalAction[]; toJSON(): unknown; toDummyRouterCalldata(): Hex; } @@ -70,6 +86,7 @@ export declare class ActionHandle { splicePayloadWord(payloadOffset: BigNumberish, source: ReturnSource): this; splicePayload(payloadOffset: BigNumberish, source: ReturnSource, length?: BigNumberish): this; patchWord(dstOffset: BigNumberish, source: ReturnSource): this; + storeResult(value?: boolean): this; } export declare class ActionRef { @@ -80,5 +97,7 @@ export declare class ActionRef { } export declare function concatHex(values: Hex[]): Hex; -export declare function encodeDummyRouterExecuteArgs(actions: Action[]): Hex; +export declare function encodeDummyRouterExecuteArgs(actions: Array): Hex; export declare function encodeWord(value: BigNumberish): Hex; +export declare function packActionInfo(action: Pick): bigint; +export declare function packSpliceInfo(splice: Splice): bigint; diff --git a/utils/openRouterExecutionBuilder.js b/utils/openRouterExecutionBuilder.js index 16746d4..69703cf 100644 --- a/utils/openRouterExecutionBuilder.js +++ b/utils/openRouterExecutionBuilder.js @@ -1,9 +1,10 @@ "use strict"; -const DUMMY_ROUTER_EXECUTE_SELECTOR = "0x8749f339"; +const DUMMY_ROUTER_EXECUTE_SELECTOR = "0xd405eacd"; const WORD_BYTES = 32; const WORD_HEX_CHARS = WORD_BYTES * 2; const UINT256_MAX = (1n << 256n) - 1n; +const UINT64_MAX = (1n << 64n) - 1n; const CallType = Object.freeze({ CALL: 0, @@ -43,16 +44,18 @@ class OpenRouterExecution { return this.callWithNative(target, payload, value); } - action({ callType, target, data = "0x", splices = [] }) { + action({ callType, target, data = "0x", splices = [], storeResult = false }) { const actionIndex = this._actions.length; const action = { callType: checkedCallType(callType), target: normalizeAddress(target), data: normalizeHex(data, "data"), + storeResult: Boolean(storeResult), splices: splices.map((splice, index) => normalizeSplice(splice, `splices[${index}]`)), }; for (const splice of action.splices) { validateSpliceForAction(actionIndex, action, splice); + this._actions[splice.sourceActionIndex].storeResult = true; } this._actions.push(action); return new ActionHandle(this, this._actions.length - 1); @@ -76,24 +79,35 @@ class OpenRouterExecution { } toActions() { + this._markSpliceSources(); + return this._actions.map(toDummyRouterAction); + } + + toLogicalActions() { + this._markSpliceSources(); return this._actions.map(cloneAction); } toJSON() { + this._markSpliceSources(); return this._actions.map((action) => ({ callType: action.callType, target: action.target, data: action.data, + storeResult: action.storeResult, + actionInfo: packActionInfo(action).toString(), splices: action.splices.map((splice) => ({ sourceActionIndex: String(splice.sourceActionIndex), srcOffset: String(splice.srcOffset), dstOffset: String(splice.dstOffset), length: String(splice.length), + spliceInfo: packSpliceInfo(splice).toString(), })), })); } toDummyRouterCalldata() { + this._markSpliceSources(); return concatHex([DUMMY_ROUTER_EXECUTE_SELECTOR, encodeDummyRouterExecuteArgs(this._actions)]); } @@ -108,8 +122,17 @@ class OpenRouterExecution { const action = this.actionAt(index); const normalized = normalizeSplice(splice, "splice"); validateSpliceForAction(index, action, normalized); + this._actions[normalized.sourceActionIndex].storeResult = true; action.splices.push(normalized); } + + _markSpliceSources() { + for (const action of this._actions) { + for (const splice of action.splices) { + this._actions[splice.sourceActionIndex].storeResult = true; + } + } + } } class ActionHandle { @@ -176,6 +199,11 @@ class ActionHandle { patchWord(dstOffset, source) { return this.spliceWord(dstOffset, source); } + + storeResult(value = true) { + this.execution.actionAt(this.index).storeResult = Boolean(value); + return this; + } } class ActionRef { @@ -198,7 +226,7 @@ class ActionRef { } function encodeDummyRouterExecuteArgs(actions) { - return concatHex([encodeWord(WORD_BYTES), encodeActionArray(actions)]); + return concatHex([encodeWord(WORD_BYTES), encodeActionArray(prepareActionsForEncoding(actions))]); } function encodeActionArray(actions) { @@ -213,14 +241,14 @@ function encodeActionArray(actions) { } function encodeActionTuple(action) { - const encodedData = encodeBytes(action.data); - const encodedSplices = encodeSpliceArray(action.splices); - const dataOffset = WORD_BYTES * 4; + const packedAction = isPackedAction(action) ? normalizePackedAction(action) : toDummyRouterAction(action); + const encodedData = encodeBytes(packedAction.data); + const encodedSplices = encodeUint256Array(packedAction.splices); + const dataOffset = WORD_BYTES * 3; const splicesOffset = dataOffset + hexByteLength(encodedData); return concatHex([ - encodeWord(action.callType), - encodeAddressWord(action.target), + encodeWord(packedAction.actionInfo), encodeWord(dataOffset), encodeWord(splicesOffset), encodedData, @@ -228,14 +256,8 @@ function encodeActionTuple(action) { ]); } -function encodeSpliceArray(splices) { - const encodedSplices = splices.flatMap((splice) => [ - encodeWord(splice.sourceActionIndex), - encodeWord(splice.srcOffset), - encodeWord(splice.dstOffset), - encodeWord(splice.length), - ]); - return concatHex([encodeWord(splices.length), ...encodedSplices]); +function encodeUint256Array(values) { + return concatHex([encodeWord(values.length), ...values.map(encodeWord)]); } function encodeBytes(value) { @@ -245,10 +267,6 @@ function encodeBytes(value) { return `0x${strip0x(encodeWord(byteLength))}${hex.padEnd(paddedLength, "0")}`; } -function encodeAddressWord(value) { - return `0x${strip0x(normalizeAddress(value)).padStart(WORD_HEX_CHARS, "0")}`; -} - function encodeWord(value) { const bigint = toBigInt(value); if (bigint < 0n || bigint > UINT256_MAX) throw new Error(`uint256 out of range: ${value}`); @@ -263,12 +281,14 @@ function normalizeSplice(splice, label) { if (!splice || typeof splice !== "object") throw new Error(`${label} must be an object`); const length = checkedIndex(splice.length, `${label}.length`); if (length === 0) throw new Error(`${label}.length must be greater than zero`); - return { + const normalized = { sourceActionIndex: checkedIndex(splice.sourceActionIndex, `${label}.sourceActionIndex`), srcOffset: checkedIndex(splice.srcOffset, `${label}.srcOffset`), dstOffset: checkedIndex(splice.dstOffset, `${label}.dstOffset`), length, }; + packSpliceInfo(normalized); + return normalized; } function validateSpliceForAction(actionIndex, action, splice) { @@ -337,10 +357,79 @@ function cloneAction(action) { callType: action.callType, target: action.target, data: action.data, + storeResult: action.storeResult, splices: action.splices.map((splice) => ({ ...splice })), }; } +function prepareActionsForEncoding(actions) { + const prepared = actions.map((action) => (isPackedAction(action) ? action : cloneAction(action))); + for (const action of prepared) { + if (isPackedAction(action)) continue; + for (const splice of action.splices) { + if (!prepared[splice.sourceActionIndex] || isPackedAction(prepared[splice.sourceActionIndex])) continue; + prepared[splice.sourceActionIndex].storeResult = true; + } + } + return prepared; +} + +function toDummyRouterAction(action) { + return { + actionInfo: packActionInfo(action).toString(), + data: action.data, + splices: action.splices.map((splice) => packSpliceInfo(splice).toString()), + }; +} + +function isPackedAction(action) { + return action && Object.prototype.hasOwnProperty.call(action, "actionInfo"); +} + +function normalizePackedAction(action) { + return { + actionInfo: encodeWord(action.actionInfo), + data: normalizeHex(action.data, "data"), + splices: (action.splices || []).map((splice, index) => encodeWordField(splice, `splices[${index}]`)), + }; +} + +function encodeWordField(value, label) { + try { + return encodeWord(value); + } catch (error) { + throw new Error(`${label}: ${error.message}`); + } +} + +function packActionInfo(action) { + return ( + BigInt(action.callType) | + (action.storeResult ? 1n << 8n : 0n) | + (addressToBigInt(action.target) << 16n) + ); +} + +function packSpliceInfo(splice) { + const sourceActionIndex = checkedUint64(splice.sourceActionIndex, "splice.sourceActionIndex"); + const srcOffset = checkedUint64(splice.srcOffset, "splice.srcOffset"); + const dstOffset = checkedUint64(splice.dstOffset, "splice.dstOffset"); + const length = checkedUint64(splice.length, "splice.length"); + return sourceActionIndex | (srcOffset << 64n) | (dstOffset << 128n) | (length << 192n); +} + +function checkedUint64(value, label) { + const bigint = toBigInt(value); + if (bigint < 0n || bigint > UINT64_MAX) { + throw new Error(`${label} must fit in uint64`); + } + return bigint; +} + +function addressToBigInt(value) { + return BigInt(normalizeAddress(value)); +} + module.exports = { ActionHandle, ActionRef, @@ -351,4 +440,6 @@ module.exports = { concatHex, encodeDummyRouterExecuteArgs, encodeWord, + packActionInfo, + packSpliceInfo, }; diff --git a/utils/openRouterExecutionBuilder.md b/utils/openRouterExecutionBuilder.md index 2670973..152a095 100644 --- a/utils/openRouterExecutionBuilder.md +++ b/utils/openRouterExecutionBuilder.md @@ -26,6 +26,20 @@ exec const calldata = exec.toDummyRouterCalldata(); ``` +`toActions()` returns the current gas-golfed `DummyRouter.Action[]` ABI shape: + +```js +[ + { + actionInfo, // packed callType | storeResult << 8 | target << 16 + data, + splices, // uint256[] packed as sourceActionIndex | srcOffset << 64 | dstOffset << 128 | length << 192 + }, +]; +``` + +Splice sources are marked as `storeResult` automatically. For an action whose returndata should be returned but is not used by a splice, call `.storeResult()` on the handle or pass `storeResult: true` to `action(...)`. + ## Offset Helpers - `spliceArg(argIndex, source)` writes a 32-byte source into a normal ABI calldata argument. It maps `argIndex` to `4 + argIndex * 32`. @@ -85,4 +99,4 @@ exec .splicePayloadWord(STARGATE_AMOUNT_OFFSET, exec.ref("bridgeAmount").returnWord()); ``` -Use `toActions()` when the caller already has an ABI encoder. Use `toDummyRouterCalldata()` when you need raw calldata for the current `DummyRouter`. +Use `toActions()` when the caller already has an ABI encoder for the current packed `DummyRouter`. Use `toLogicalActions()` for the readable builder shape. Use `toDummyRouterCalldata()` when you need raw calldata for the current `DummyRouter`. From 76250698646bed5b3c85ccb74c515a8698998dd8 Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 12 May 2026 19:26:45 +0400 Subject: [PATCH 16/69] feat: port latest modular executor to combined --- scripts/e2e/bridgeViaRelay.ts | 46 +---- scripts/e2e/config.ts | 6 +- scripts/e2e/swapBridgeViaArbitrumNative.ts | 69 ++----- scripts/e2e/swapBridgeViaCctp.ts | 80 ++------ scripts/e2e/swapBridgeViaStargateNative.ts | 73 ++----- scripts/e2e/utils/contractTypes.ts | 28 +-- .../e2e/utils/modularActionsBuilder/README.md | 17 +- .../utils/modularActionsBuilder/index.d.ts | 17 +- .../e2e/utils/modularActionsBuilder/index.js | 23 +-- scripts/e2e/utils/routerAbi.ts | 4 +- src/combined/BungeeOpenRouterV2.sol | 180 +++++++++--------- src/combined/BungeeOpenRouterV2Unchecked.sol | 146 +++++++------- test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 45 ++--- ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 53 +++--- 14 files changed, 290 insertions(+), 497 deletions(-) rename utils/openRouterExecutionBuilder.md => scripts/e2e/utils/modularActionsBuilder/README.md (84%) rename utils/openRouterExecutionBuilder.d.ts => scripts/e2e/utils/modularActionsBuilder/index.d.ts (86%) rename utils/openRouterExecutionBuilder.js => scripts/e2e/utils/modularActionsBuilder/index.js (96%) diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index 0b62e24..f609d5d 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -41,10 +41,10 @@ import { import { execViaAH } from './utils/allowanceHolder'; import { encodeApprove, encodeTransfer, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; +import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; +import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, - Action, - CallType, NO_FEE, NO_SWAP, ZERO_ADDRESS, @@ -207,7 +207,7 @@ function buildModularActions( relaySpender: string, depositTarget: string, depositData: string, -): Action[] { +): ModularAction[] { // AH.transferFrom(token, owner, recipient, amount) = 0x15dacbea const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -219,40 +219,12 @@ function buildModularActions( inputAmount, ]); - return [ - // 0: pull AAVE from user via AllowanceHolder.transferFrom - { - callType: CallType.CALL, - target: ALLOWANCE_HOLDER, - value: 0n, - data: ahTransferFromData, - splices: [], - }, - // 1: send pre-bridge fee to signer in AAVE - { - callType: CallType.CALL, - target: TOKENS.AAVE_ARB, - value: 0n, - data: encodeTransfer(signerAddress, feeAmount), - splices: [], - }, - // 2: approve Relay spender for bridgeAmount - { - callType: CallType.CALL, - target: TOKENS.AAVE_ARB, - value: 0n, - data: encodeApprove(relaySpender, bridgeAmount), - splices: [], - }, - // 3: call Relay deposit — amount already encoded in depositData - { - callType: CallType.CALL, - target: depositTarget, - value: 0n, - data: depositData, - splices: [], - }, - ]; + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.AAVE_ARB, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.AAVE_ARB, encodeApprove(relaySpender, bridgeAmount)); + exec.call(depositTarget, depositData); + return exec.toActions(); } // ─── Main ───────────────────────────────────────────────────────────────────── diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 9b74376..6906384 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -3,7 +3,6 @@ * used across all e2e scripts. */ import * as dotenv from 'dotenv'; -import { MaxUint256 } from 'ethers'; dotenv.config(); // ─── Chain IDs ─────────────────────────────────────────────────────────────── @@ -19,12 +18,9 @@ export const CHAIN_IDS = { /** 0x AllowanceHolder — same address on every EVM chain */ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; -/** Deployed BungeeOpenRouterV2Unchecked instance (set via env after deployment) */ +/** Deployed combined unchecked router instance (set via env after deployment) */ export const ROUTER_ADDRESS: string = '0x98381Fb4dC5c2046558236857181F4e34a9088dC'; -/** Sentinel used in modular actions to forward address(this).balance as msg.value */ -export const MAX_UINT256 = MaxUint256; - /** Standard ERC-20 "native" sentinel used by CurrencyLib */ export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index 44d4fc8..a6557ef 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -12,8 +12,8 @@ * - Monolithic: swap AAVE→ETH (balance delta on NATIVE), take ETH fee, * call Arbitrum inbox with useFinalAmountAsValue=true so finalAmount * becomes msg.value on the depositEth call. - * - Modular: pull → approve(oo) → swap(oo) → send ETH fee via low-level call → - * depositEth with value=MAX_UINT256 sentinel (forwards address(this).balance). + * - Modular: pull → approve(oo) → swap(oo) → send ETH fee via CALL_WITH_NATIVE → + * depositEth via CALL_WITH_NATIVE. * 5. Call AllowanceHolder.exec with msg.value=0 (AAVE is the input token, not ETH). * * Uses the signer’s full AAVE balance on Ethereum mainnet as swap input. @@ -50,13 +50,12 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; +import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; +import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, - Action, - CallType, NO_FEE, ZERO_ADDRESS, - USE_CONTRACT_BALANCE, } from './utils/contractTypes'; // ─── Arbitrum retryable fee estimation ─────────────────────────────────────── @@ -227,18 +226,18 @@ function buildMonolithicExecution( * [0] Pull AAVE via AH.transferFrom * [1] Approve OpenOcean router for inputAmount * [2] Call OpenOcean to swap AAVE → ETH (lands in router as ETH) - * [3] Send ETH fee to signer via low-level call (value=feeAmount) - * [4] Call Arbitrum inbox depositEth() with value=MAX_UINT256 sentinel - * so _dispatchAction forwards address(this).balance (all remaining ETH) + * [3] Send ETH fee to signer via CALL_WITH_NATIVE + * [4] Call Arbitrum inbox depositEth() via CALL_WITH_NATIVE */ function buildModularActions( signerAddress: string, routerAddress: string, inputAmount: bigint, feeAmount: bigint, + bridgeValue: bigint, ooRouterAddress: string, swapData: string, -): Action[] { +): ModularAction[] { const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); @@ -249,50 +248,13 @@ function buildModularActions( inputAmount, ]); - return [ - // 0: pull AAVE from user via AllowanceHolder - { - callType: CallType.CALL, - target: ALLOWANCE_HOLDER, - value: 0n, - data: ahTransferFromData, - splices: [], - }, - // 1: approve OpenOcean to spend AAVE - { - callType: CallType.CALL, - target: TOKENS.AAVE_ETH, - value: 0n, - data: encodeApprove(ooRouterAddress, inputAmount), - splices: [], - }, - // 2: swap AAVE → ETH via OpenOcean (ETH lands in the router) - { - callType: CallType.CALL, - target: ooRouterAddress, - value: 0n, - data: swapData, - splices: [], - }, - // 3: send ETH fee to signer — target receives feeAmount as msg.value via CALL - // The signer EOA must be payable; any standard address accepts ETH. - { - callType: CallType.CALL, - target: signerAddress, - value: feeAmount, - data: '0x', // empty calldata = plain ETH transfer - splices: [], - }, - // 4: deposit remaining ETH to Arbitrum via inbox.depositEth() - // USE_CONTRACT_BALANCE sentinel → _dispatchAction substitutes address(this).balance - { - callType: CallType.CALL, - target: ARBITRUM_INBOX, - value: USE_CONTRACT_BALANCE, - data: buildDepositEthCalldata(), - splices: [], - }, - ]; + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouterAddress, inputAmount)); + exec.call(ooRouterAddress, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); + return exec.toActions(); } // ─── Main ───────────────────────────────────────────────────────────────────── @@ -366,6 +328,7 @@ async function main() { ROUTER_ADDRESS, inputAmount, feeAmount, + minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n, ooRouterAddress, swapData, ); diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index c103efd..91ced86 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -42,10 +42,10 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; +import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; +import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, - Action, - CallType, NO_FEE, ZERO_ADDRESS, } from './utils/contractTypes'; @@ -220,7 +220,7 @@ function buildModularActions( swapData: string, depositForBurnData: string, tokenMessenger: string, -): Action[] { +): ModularAction[] { const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); @@ -231,71 +231,15 @@ function buildModularActions( inputAmount, ]); - return [ - // 0: pull AAVE from user via AH - { - callType: CallType.CALL, - target: ALLOWANCE_HOLDER, - value: 0n, - data: ahTransferFromData, - splices: [], - }, - // 1: approve OpenOcean to spend AAVE - { - callType: CallType.CALL, - target: TOKENS.AAVE_ARB, - value: 0n, - data: encodeApprove(ooRouterAddress, inputAmount), - splices: [], - }, - // 2: swap AAVE → USDC via OpenOcean - { - callType: CallType.CALL, - target: ooRouterAddress, - value: 0n, - data: swapData, - splices: [], - }, - // 3: send post-swap fee in USDC to signer - { - callType: CallType.CALL, - target: TOKENS.USDC_ARB, - value: 0n, - data: encodeTransfer(signerAddress, feeAmount), - splices: [], - }, - // 4: approve TOKEN_MESSENGER for unlimited USDC (router holds exact balance) - { - callType: CallType.CALL, - target: TOKENS.USDC_ARB, - value: 0n, - data: encodeApprove(tokenMessenger, ethers.MaxUint256), - splices: [], - }, - // 5: staticcall USDC.balanceOf(router) → prevReturn = ABI-encoded uint256 balance - { - callType: CallType.STATICCALL, - target: TOKENS.USDC_ARB, - value: 0n, - data: encodeBalanceOf(routerAddress), - splices: [], - }, - // 6: depositForBurn — splice the 32-byte balance from prevReturn into the - // amount field at dstOffset=4 (first param, after the 4-byte selector) - { - callType: CallType.CALL, - target: tokenMessenger, - value: 0n, - data: depositForBurnData, - splices: [ - { - srcOffset: 0n, // read from start of prevReturn (the ABI uint256) - dstOffset: 4n, // write into depositForBurn calldata after selector - length: 32n, // uint256 = 32 bytes - }, - ], - }, - ]; + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.AAVE_ARB, encodeApprove(ooRouterAddress, inputAmount)); + exec.call(ooRouterAddress, swapData); + exec.call(TOKENS.USDC_ARB, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_ARB, encodeApprove(tokenMessenger, ethers.MaxUint256)); + const balance = exec.staticCall(TOKENS.USDC_ARB, encodeBalanceOf(routerAddress)); + exec.call(tokenMessenger, depositForBurnData).spliceArg(0, balance.returnWord()); + return exec.toActions(); } // ─── Main ───────────────────────────────────────────────────────────────────── diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index f25e50e..f30a444 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -11,7 +11,7 @@ * - Monolithic: pull USDC via AH → swap USDC→ETH → post-fee (ETH to signer) → * Stargate send (useFinalAmountAsValue=true, static amountLD) * - Modular: pull USDC → approve OO → swap → ETH fee transfer → - * Stargate send (value=USE_CONTRACT_BALANCE, static amountLD) + * Stargate send via CALL_WITH_NATIVE with static amountLD/msg.value * 6. Ensure AllowanceHolder ERC20 allowance for USDC. * 7. Execute AllowanceHolder.exec, forwarding nativeFee as msg.value so the * router has enough ETH to cover both the bridge amount and the LZ fee. @@ -50,10 +50,10 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; +import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; +import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, - Action, - CallType, NO_FEE, ZERO_ADDRESS, } from './utils/contractTypes'; @@ -286,13 +286,10 @@ function buildMonolithicExecution( * [0] AH.transferFrom USDC (pull from user) * [1] USDC.approve(ooRouter, inputAmount) * [2] Call OO router to swap USDC → ETH - * [3] Send feeAmount ETH to signer (plain ETH transfer) - * [4] Stargate send() with value=MaxUint256 (USE_CONTRACT_BALANCE sentinel) + * [3] Send feeAmount ETH to signer via CALL_WITH_NATIVE + * [4] Stargate send() via CALL_WITH_NATIVE * - * The USE_CONTRACT_BALANCE sentinel causes _dispatchAction to substitute - * address(this).balance as msg.value. After action[3], the router holds - * (actualFinalAmount + nativeFee) ETH — satisfying Stargate's requirement. - * amountLD in the calldata is pre-encoded and does not need splicing. + * amountLD in the calldata and msg.value are pre-encoded and do not need splicing. * * @param signerAddress Signer/recipient address * @param routerAddress Router contract address (receives ETH from swap) @@ -307,10 +304,11 @@ function buildModularActions( routerAddress: string, inputAmount: bigint, feeAmount: bigint, + bridgeValue: bigint, ooRouterAddress: string, swapData: string, stargateData: string, -): Action[] { +): ModularAction[] { const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); @@ -321,51 +319,13 @@ function buildModularActions( inputAmount, ]); - return [ - // 0: pull USDC from user via AllowanceHolder - { - callType: CallType.CALL, - target: ALLOWANCE_HOLDER, - value: 0n, - data: ahTransferFromData, - splices: [], - }, - // 1: approve OpenOcean to spend USDC - { - callType: CallType.CALL, - target: TOKENS.USDC_ARB, - value: 0n, - data: encodeApprove(ooRouterAddress, inputAmount), - splices: [], - }, - // 2: swap USDC → native ETH via OpenOcean (ETH lands in router) - { - callType: CallType.CALL, - target: ooRouterAddress, - value: 0n, - data: swapData, - splices: [], - }, - // 3: send post-swap fee in ETH to signer (plain ETH transfer) - { - callType: CallType.CALL, - target: signerAddress, - value: feeAmount, - data: '0x', - splices: [], - }, - // 4: bridge ETH via Stargate Native Pool - // value=MaxUint256 → _dispatchAction substitutes address(this).balance. - // After action[3] the router has (actualFinalAmount + nativeFee) ETH, - // which satisfies Stargate's msg.value >= amountLD + nativeFee constraint. - { - callType: CallType.CALL, - target: STARGATE_NATIVE_ARB, - value: ethers.MaxUint256, // USE_CONTRACT_BALANCE sentinel - data: stargateData, - splices: [], - }, - ]; + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.USDC_ARB, encodeApprove(ooRouterAddress, inputAmount)); + exec.call(ooRouterAddress, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(STARGATE_NATIVE_ARB, stargateData, bridgeValue); + return exec.toActions(); } // ─── Main ───────────────────────────────────────────────────────────────────── @@ -467,6 +427,7 @@ async function main() { ROUTER_ADDRESS, inputAmount, feeAmount, + amountLD + nativeFeeWithBuffer, ooRouterAddress, swapData, stargateData, @@ -496,7 +457,7 @@ async function main() { // msg.value = nativeFeeWithBuffer (forwarded to the router alongside USDC pull). // The router needs this ETH in its balance so that after the swap: // router.balance = actualFinalAmount + nativeFeeWithBuffer - // Stargate: msg.value = actualFinalAmount >= amountLD + nativeFee ✓ + // Stargate action msg.value = prequoted amountLD + nativeFeeWithBuffer console.log( `Sending AllowanceHolder.exec with msg.value = ${ethers.formatEther(nativeFeeWithBuffer)} ETH (LZ fee)...`, ); diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index bc33e92..1a8edda 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -1,6 +1,6 @@ /** * TypeScript interfaces that mirror every Solidity struct in - * BungeeOpenRouterV2Unchecked. The order and field names must match the ABI + * Combined unchecked router. The order and field names must match the ABI * produced by the compiler so that ethers.js can encode them correctly. */ @@ -43,36 +43,10 @@ export interface MonolithicExecution { bridge: BridgeData; } -// ─── Modular execution types ────────────────────────────────────────────────── - -export enum CallType { - CALL = 0, - DELEGATECALL = 1, - STATICCALL = 2, -} - -export interface Splice { - srcOffset: bigint; - dstOffset: bigint; - length: bigint; -} - -export interface Action { - callType: CallType; - target: string; - /** Use MAX_UINT256 sentinel to forward address(this).balance */ - value: bigint; - data: string; - splices: Splice[]; -} - // ─── Sentinel / zero helpers ────────────────────────────────────────────────── export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; -/** Sentinel value: _dispatchAction forwards address(this).balance as msg.value */ -export const USE_CONTRACT_BALANCE = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); - /** Convenience: empty fee (no fee taken) */ export const NO_FEE: FeeData = { receiver: ZERO_ADDRESS, amount: 0n }; diff --git a/utils/openRouterExecutionBuilder.md b/scripts/e2e/utils/modularActionsBuilder/README.md similarity index 84% rename from utils/openRouterExecutionBuilder.md rename to scripts/e2e/utils/modularActionsBuilder/README.md index 152a095..81211e3 100644 --- a/utils/openRouterExecutionBuilder.md +++ b/scripts/e2e/utils/modularActionsBuilder/README.md @@ -1,11 +1,12 @@ -# OpenRouter Execution Builder +# Modular Actions Builder -Dependency-free helper for formatting `DummyRouter.execute(Action[])` payloads from provider SDK/API calldata. +Dependency-free helper for formatting packed `performModularExecution(Action[])` +payloads from provider SDK/API calldata. ```js -const { OpenRouterExecution } = require("./openRouterExecutionBuilder"); +const { ModularActionsBuilder } = require("./modularActionsBuilder/index"); -const exec = new OpenRouterExecution({ +const exec = new ModularActionsBuilder({ routeId: "openocean-stargate-native", chainId: 42161, }); @@ -23,10 +24,10 @@ exec .as("feeTransfer") .valueFrom(exec.ref("routeFee").returnWord()); -const calldata = exec.toDummyRouterCalldata(); +const calldata = exec.toCalldata(); ``` -`toActions()` returns the current gas-golfed `DummyRouter.Action[]` ABI shape: +`toActions()` returns the packed modular `Action[]` ABI shape: ```js [ @@ -99,4 +100,6 @@ exec .splicePayloadWord(STARGATE_AMOUNT_OFFSET, exec.ref("bridgeAmount").returnWord()); ``` -Use `toActions()` when the caller already has an ABI encoder for the current packed `DummyRouter`. Use `toLogicalActions()` for the readable builder shape. Use `toDummyRouterCalldata()` when you need raw calldata for the current `DummyRouter`. +Use `toActions()` when the caller already has an ABI encoder for the packed modular action tuple. Use +`toLogicalActions()` for the readable builder shape. Use `toCalldata()` when you need raw +`performModularExecution(Action[])` calldata. diff --git a/utils/openRouterExecutionBuilder.d.ts b/scripts/e2e/utils/modularActionsBuilder/index.d.ts similarity index 86% rename from utils/openRouterExecutionBuilder.d.ts rename to scripts/e2e/utils/modularActionsBuilder/index.d.ts index 3c7d141..06b6121 100644 --- a/utils/openRouterExecutionBuilder.d.ts +++ b/scripts/e2e/utils/modularActionsBuilder/index.d.ts @@ -1,4 +1,4 @@ -export type Hex = `0x${string}`; +export type Hex = string; export type Address = Hex; export type BigNumberish = bigint | number | string; @@ -27,7 +27,7 @@ export interface LogicalAction { splices: Splice[]; } -export interface DummyRouterAction { +export interface ModularAction { actionInfo: BigNumberish; data: Hex; splices: BigNumberish[]; @@ -35,7 +35,7 @@ export interface DummyRouterAction { export type Action = LogicalAction; -export declare const DUMMY_ROUTER_EXECUTE_SELECTOR: "0xd405eacd"; +export declare const PERFORM_MODULAR_EXECUTION_SELECTOR: "0x4f85c3a5"; export declare const CallType: Readonly<{ CALL: 0; @@ -48,7 +48,7 @@ export declare const Offset: Readonly<{ nativePayload(payloadOffset: BigNumberish): number; }>; -export declare class OpenRouterExecution { +export declare class ModularActionsBuilder { context: ExecutionContext; constructor(context?: ExecutionContext); call(target: Address, data: Hex): ActionHandle; @@ -64,14 +64,14 @@ export declare class OpenRouterExecution { }): ActionHandle; ref(labelOrIndex: string | BigNumberish): ActionRef; actionAt(index: BigNumberish): LogicalAction; - toActions(): DummyRouterAction[]; + toActions(): ModularAction[]; toLogicalActions(): LogicalAction[]; toJSON(): unknown; - toDummyRouterCalldata(): Hex; + toCalldata(): Hex; } export declare class ActionHandle { - readonly execution: OpenRouterExecution; + readonly execution: ModularActionsBuilder; readonly index: number; as(label: string): this; label(label: string): this; @@ -97,7 +97,8 @@ export declare class ActionRef { } export declare function concatHex(values: Hex[]): Hex; -export declare function encodeDummyRouterExecuteArgs(actions: Array): Hex; +export declare function encodePerformModularExecutionArgs(actions: Array): Hex; export declare function encodeWord(value: BigNumberish): Hex; export declare function packActionInfo(action: Pick): bigint; export declare function packSpliceInfo(splice: Splice): bigint; +export declare function toModularAction(action: LogicalAction): ModularAction; diff --git a/utils/openRouterExecutionBuilder.js b/scripts/e2e/utils/modularActionsBuilder/index.js similarity index 96% rename from utils/openRouterExecutionBuilder.js rename to scripts/e2e/utils/modularActionsBuilder/index.js index 69703cf..b0ba0d2 100644 --- a/utils/openRouterExecutionBuilder.js +++ b/scripts/e2e/utils/modularActionsBuilder/index.js @@ -1,6 +1,6 @@ "use strict"; -const DUMMY_ROUTER_EXECUTE_SELECTOR = "0xd405eacd"; +const PERFORM_MODULAR_EXECUTION_SELECTOR = "0x4f85c3a5"; const WORD_BYTES = 32; const WORD_HEX_CHARS = WORD_BYTES * 2; const UINT256_MAX = (1n << 256n) - 1n; @@ -17,7 +17,7 @@ const Offset = Object.freeze({ nativePayload: (payloadOffset) => WORD_BYTES + checkedIndex(payloadOffset, "payloadOffset"), }); -class OpenRouterExecution { +class ModularActionsBuilder { constructor(context = {}) { this.context = { ...context }; this._actions = []; @@ -80,7 +80,7 @@ class OpenRouterExecution { toActions() { this._markSpliceSources(); - return this._actions.map(toDummyRouterAction); + return this._actions.map(toModularAction); } toLogicalActions() { @@ -106,9 +106,9 @@ class OpenRouterExecution { })); } - toDummyRouterCalldata() { + toCalldata() { this._markSpliceSources(); - return concatHex([DUMMY_ROUTER_EXECUTE_SELECTOR, encodeDummyRouterExecuteArgs(this._actions)]); + return concatHex([PERFORM_MODULAR_EXECUTION_SELECTOR, encodePerformModularExecutionArgs(this._actions)]); } _label(index, label) { @@ -225,7 +225,7 @@ class ActionRef { } } -function encodeDummyRouterExecuteArgs(actions) { +function encodePerformModularExecutionArgs(actions) { return concatHex([encodeWord(WORD_BYTES), encodeActionArray(prepareActionsForEncoding(actions))]); } @@ -241,7 +241,7 @@ function encodeActionArray(actions) { } function encodeActionTuple(action) { - const packedAction = isPackedAction(action) ? normalizePackedAction(action) : toDummyRouterAction(action); + const packedAction = isPackedAction(action) ? normalizePackedAction(action) : toModularAction(action); const encodedData = encodeBytes(packedAction.data); const encodedSplices = encodeUint256Array(packedAction.splices); const dataOffset = WORD_BYTES * 3; @@ -374,7 +374,7 @@ function prepareActionsForEncoding(actions) { return prepared; } -function toDummyRouterAction(action) { +function toModularAction(action) { return { actionInfo: packActionInfo(action).toString(), data: action.data, @@ -434,12 +434,13 @@ module.exports = { ActionHandle, ActionRef, CallType, - DUMMY_ROUTER_EXECUTE_SELECTOR, + PERFORM_MODULAR_EXECUTION_SELECTOR, Offset, - OpenRouterExecution, + ModularActionsBuilder, concatHex, - encodeDummyRouterExecuteArgs, + encodePerformModularExecutionArgs, encodeWord, packActionInfo, packSpliceInfo, + toModularAction, }; diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index 5ed4e64..ef694df 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -1,5 +1,5 @@ /** - * ABI fragment for BungeeOpenRouterV2Unchecked — only the two entrypoints + * ABI fragment for the combined unchecked router — only the two entrypoints * called from e2e scripts. Structs must exactly match the Solidity definitions. */ export const ROUTER_ABI = [ @@ -16,6 +16,6 @@ export const ROUTER_ABI = [ // Modular path `function performModularExecution( - (uint8 callType, address target, uint256 value, bytes data, (uint256 srcOffset, uint256 dstOffset, uint256 length)[] splices)[] actions + (uint256 actionInfo, bytes data, uint256[] splices)[] actions ) external payable`, ] as const; diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol index bd3e5e9..2d7d635 100644 --- a/src/combined/BungeeOpenRouterV2.sol +++ b/src/combined/BungeeOpenRouterV2.sol @@ -18,13 +18,10 @@ import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; /// optional post-swap fee, bridge call with multi-position amount /// splicing. Suitable for the vast majority of routes. /// -/// 2. `performModularExecution` — generic action loop (identical to -/// `BungeeOpenRouterModular`). Each `Action` carries a list of -/// `Splice`s that copy byte ranges from the previous action's -/// returndata into this action's calldata before dispatch. Use this -/// for routes that need more than one bridge call, non-standard step -/// ordering, or multiple amount fields patched from a single prior -/// return value. +/// 2. `performModularExecution` — generic action loop. Each `Action` +/// carries packed call metadata and packed splices that copy byte +/// ranges from any earlier stored action result into this action's +/// calldata before dispatch. /// /// Fund pulls always go through 0x AllowanceHolder (transient-storage /// allowance). The `_msgSender() == user` guard ensures the AH @@ -97,25 +94,17 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { enum CallType { CALL, - DELEGATECALL, - STATICCALL - } - - /// @notice Byte-range copy from the previous action's returndata into this - /// action's calldata, applied before the action is dispatched. - struct Splice { - uint256 srcOffset; // offset within the previous action's returndata - uint256 dstOffset; // offset within this action's `data` - uint256 length; // number of bytes to copy + STATICCALL, + CALL_WITH_NATIVE } /// @notice One step in the modular execution pipeline. + /// @dev `actionInfo` packs call type in bits [0:8), store-result flag in + /// bits [8:16), and target address in bits [16:176). struct Action { - CallType callType; - address target; - uint256 value; // ETH forwarded; must be zero for DELEGATECALL / STATICCALL - bytes data; // base calldata, patched in-place by splices before dispatch - Splice[] splices; // applied BEFORE this action runs + uint256 actionInfo; + bytes data; + uint256[] splices; } /// @notice Signed payload for the modular execution path. @@ -134,9 +123,10 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { error InvalidExecution(); error CallerNotSignedUser(); error InsufficientMsgValue(); - error ValueOnNonCall(); - error EmptyActions(); - error UnknownCallType(); + error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); + error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); + error CallFailed(uint256 actionIndex, bytes returndata); + error MissingNativeValue(uint256 actionIndex); // ========================================================================= // Constructor @@ -174,10 +164,14 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { * @dev The signed digest covers the entire action set, so the caller cannot * reorder, retarget, or strip splices from any action. */ - function performModularExecution(ModularExecution calldata exec, bytes calldata signature) external payable { + function performModularExecution(ModularExecution calldata exec, bytes calldata signature) + external + payable + returns (bytes[] memory results) + { bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); + results = _performActions(exec.actions); } // ========================================================================= @@ -240,7 +234,10 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { } /// @dev Balance-delta swap helper; split out to keep _runMonolithic under 100 lines. - function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { + function _performSwap(MonolithicExecution calldata exec) + internal + returns (address finalToken, uint256 finalAmount) + { uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { @@ -318,82 +315,75 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { // Internal: modular action loop // ========================================================================= - /** - * @notice Runs a signed sequence of actions, applying returndata splices - * from each step into the calldata of the next before dispatch. - */ - function _performActions(Action[] calldata actions) internal { - if (actions.length == 0) { - revert EmptyActions(); - } + function _performActions(Action[] calldata actions) internal returns (bytes[] memory results) { + uint256 actionsLength = actions.length; + results = new bytes[](actionsLength); + + for (uint256 i; i < actionsLength;) { + Action calldata action = actions[i]; + bytes memory callData = action.data; + + uint256 splicesLength = action.splices.length; + for (uint256 j; j < splicesLength;) { + uint256 spliceInfo = action.splices[j]; + uint256 sourceActionIndex = uint64(spliceInfo); + if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); + + uint256 srcOffset = uint64(spliceInfo >> 64); + uint256 dstOffset = uint64(spliceInfo >> 128); + uint256 length = spliceInfo >> 192; + bytes memory source = results[sourceActionIndex]; + if (srcOffset + length > source.length || dstOffset + length > callData.length) { + revert SpliceOutOfBounds(i, j); + } + + assembly ("memory-safe") { + mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) + } - bytes memory prevReturn; // empty on first action; splice on action[0] is illegal - for (uint256 i = 0; i < actions.length;) { - Action calldata a = actions[i]; - bytes memory data = a.data; - - // apply splices: copy byte ranges from prevReturn into this action's data - uint256 spLen = a.splices.length; - for (uint256 j = 0; j < spLen;) { - Splice calldata sp = a.splices[j]; - BytesSpliceLib.spliceBytes({ - dst: data, // this action's calldata (base is signed; patched before dispatch) - dstOffset: sp.dstOffset, // write `length` bytes into `dst` starting here - src: prevReturn, // read from the previous action's returndata - srcOffset: sp.srcOffset, // copy slice starting at this offset in `src` - length: sp.length // number of bytes to copy (overwrites same span in `dst`) - }); unchecked { ++j; } } - prevReturn = _dispatchAction(a.callType, a.target, a.value, data); - unchecked { - ++i; - } - } - } - - /** - * @notice Dispatches a single action with the given call type; bubbles revert. - * @dev Named `_dispatchAction` (rather than overloading `_performAction`) - * to keep the CALL-only base helper in `OpenRouterAuthBase` distinct - * from this three-way dispatcher. - * - * `value == type(uint256).max` is a sentinel meaning "use entire contract - * ETH balance". This lets modular callers forward the full native output - * of a swap to a native-token bridge without knowing the exact amount at - * calldata-build time. - */ - function _dispatchAction(CallType callType, address target, uint256 value, bytes memory data) - internal - returns (bytes memory ret) - { - if (value == type(uint256).max) { - value = address(this).balance; - } + bool success; + uint256 actionInfo = action.actionInfo; + bool storeResult = (actionInfo & 0xff00) != 0; + uint256 callType = actionInfo & 0xff; + address target = address(uint160(actionInfo >> 16)); - bool ok; - if (callType == CallType.CALL) { - (ok, ret) = target.call{value: value}(data); - } else if (callType == CallType.DELEGATECALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.delegatecall(data); - } else if (callType == CallType.STATICCALL) { - if (value != 0) { - revert ValueOnNonCall(); + if (callType == uint256(CallType.STATICCALL)) { + assembly ("memory-safe") { + success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) + } + } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { + if (callData.length < 32) revert MissingNativeValue(i); + uint256 callValue; + uint256 payloadLength = callData.length - 32; + assembly ("memory-safe") { + callValue := mload(add(callData, 0x20)) + success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) + } + } else { + assembly ("memory-safe") { + success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) + } } - (ok, ret) = target.staticcall(data); - } else { - revert UnknownCallType(); - } - if (!ok) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) + if (!success || storeResult) { + bytes memory ret; + assembly ("memory-safe") { + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) + returndatacopy(add(ret, 0x20), 0, returnDataSize) + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) + } + if (!success) revert CallFailed(i, ret); + results[i] = ret; + } + unchecked { + ++i; } } } diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index d0483d5..3f25502 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -82,22 +82,14 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { enum CallType { CALL, - DELEGATECALL, - STATICCALL - } - - struct Splice { - uint256 srcOffset; - uint256 dstOffset; - uint256 length; + STATICCALL, + CALL_WITH_NATIVE } struct Action { - CallType callType; - address target; - uint256 value; + uint256 actionInfo; bytes data; - Splice[] splices; + uint256[] splices; } // ========================================================================= @@ -109,9 +101,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { error InvalidExecution(); error CallerNotSignedUser(); error InsufficientMsgValue(); - error ValueOnNonCall(); - error EmptyActions(); - error UnknownCallType(); + error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); + error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); + error CallFailed(uint256 actionIndex, bytes returndata); + error MissingNativeValue(uint256 actionIndex); // ========================================================================= // Constructor @@ -145,8 +138,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * @notice Runs a sequence of generic actions with optional returndata * splicing between steps. No signature verification. */ - function performModularExecution(Action[] calldata actions) external payable { - _performActions(actions); + function performModularExecution(Action[] calldata actions) external payable returns (bytes[] memory results) { + results = _performActions(actions); } // ========================================================================= @@ -209,7 +202,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } /// @dev Balance-delta swap helper. - function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { + function _performSwap(MonolithicExecution calldata exec) + internal + returns (address finalToken, uint256 finalAmount) + { uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { @@ -287,69 +283,75 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Internal: modular action loop // ========================================================================= - function _performActions(Action[] calldata actions) internal { - if (actions.length == 0) { - revert EmptyActions(); - } + function _performActions(Action[] calldata actions) internal returns (bytes[] memory results) { + uint256 actionsLength = actions.length; + results = new bytes[](actionsLength); + + for (uint256 i; i < actionsLength;) { + Action calldata action = actions[i]; + bytes memory callData = action.data; + + uint256 splicesLength = action.splices.length; + for (uint256 j; j < splicesLength;) { + uint256 spliceInfo = action.splices[j]; + uint256 sourceActionIndex = uint64(spliceInfo); + if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); + + uint256 srcOffset = uint64(spliceInfo >> 64); + uint256 dstOffset = uint64(spliceInfo >> 128); + uint256 length = spliceInfo >> 192; + bytes memory source = results[sourceActionIndex]; + if (srcOffset + length > source.length || dstOffset + length > callData.length) { + revert SpliceOutOfBounds(i, j); + } + + assembly ("memory-safe") { + mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) + } - bytes memory prevReturn; - for (uint256 i = 0; i < actions.length;) { - Action calldata a = actions[i]; - bytes memory data = a.data; - - uint256 spLen = a.splices.length; - for (uint256 j = 0; j < spLen;) { - Splice calldata sp = a.splices[j]; - BytesSpliceLib.spliceBytes({ - dst: data, // this action's calldata (patched before dispatch) - dstOffset: sp.dstOffset, // write `length` bytes into `dst` starting here - src: prevReturn, // read from the previous action's returndata - srcOffset: sp.srcOffset, // copy slice starting at this offset in `src` - length: sp.length // number of bytes to copy (overwrites same span in `dst`) - }); unchecked { ++j; } } - prevReturn = _dispatchAction(a.callType, a.target, a.value, data); - unchecked { - ++i; - } - } - } + bool success; + uint256 actionInfo = action.actionInfo; + bool storeResult = (actionInfo & 0xff00) != 0; + uint256 callType = actionInfo & 0xff; + address target = address(uint160(actionInfo >> 16)); - function _dispatchAction(CallType callType, address target, uint256 value, bytes memory data) - internal - returns (bytes memory ret) - { - // type(uint256).max is a sentinel meaning "use entire contract ETH balance". - // This lets modular callers forward the full native output of a swap to a - // native-token bridge without knowing the exact amount at calldata-build time. - if (value == type(uint256).max) { - value = address(this).balance; - } - - bool ok; - if (callType == CallType.CALL) { - (ok, ret) = target.call{value: value}(data); - } else if (callType == CallType.DELEGATECALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.delegatecall(data); - } else if (callType == CallType.STATICCALL) { - if (value != 0) { - revert ValueOnNonCall(); + if (callType == uint256(CallType.STATICCALL)) { + assembly ("memory-safe") { + success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) + } + } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { + if (callData.length < 32) revert MissingNativeValue(i); + uint256 callValue; + uint256 payloadLength = callData.length - 32; + assembly ("memory-safe") { + callValue := mload(add(callData, 0x20)) + success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) + } + } else { + assembly ("memory-safe") { + success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) + } } - (ok, ret) = target.staticcall(data); - } else { - revert UnknownCallType(); - } - if (!ok) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) + if (!success || storeResult) { + bytes memory ret; + assembly ("memory-safe") { + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) + returndatacopy(add(ret, 0x20), 0, returnDataSize) + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) + } + if (!success) revert CallFailed(i, ret); + results[i] = ret; + } + unchecked { + ++i; } } } diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 43606d4..537f5fa 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {DummyRouter} from "../../src/dummyRouter.sol"; +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; import {AcrossERC20AmountManipulator} from "../../src/manipulators/AcrossERC20AmountManipulator.sol"; interface ISpokePool { @@ -49,7 +49,7 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { vm.warp(FORK_BLOCK_TIMESTAMP); } - DummyRouter router = _routerAtFixtureAddress(); + Router router = _routerAtFixtureAddress(); AcrossERC20AmountManipulator manipulator = new AcrossERC20AmountManipulator(); if (bytes(rpcUrl).length == 0) { emit log("Set ARBITRUM_RPC to execute this fork PoC."); @@ -62,14 +62,14 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { deal(ARBITRUM_USDC, address(router), inputAmount); deal(ARBITRUM_WETH, address(router), 0); - DummyRouter.Action[] memory actions = + Router.Action[] memory actions = _buildActions(manipulator, inputAmount, bridgeFee, vm.parseBytes(OPENOCEAN_SWAP_CALLDATA)); uint256 spokePoolWethBefore = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL); uint256 gasBeforeExecute = gasleft(); - bytes[] memory results = router.execute(actions); + bytes[] memory results = router.performModularExecution(actions); uint256 executeGasUsed = gasBeforeExecute - gasleft(); - emit log_named_uint("router.execute gas used", executeGasUsed); + emit log_named_uint("router.performModularExecution gas used", executeGasUsed); _assertPocResult(router, bridgeFee, spokePoolWethBefore, results[2]); } @@ -79,23 +79,23 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { uint256 inputAmount, uint256 bridgeFee, bytes memory swapCalldata - ) internal view returns (DummyRouter.Action[] memory actions) { - actions = new DummyRouter.Action[](5); + ) internal view returns (Router.Action[] memory actions) { + actions = new Router.Action[](5); actions[0] = _action( - DummyRouter.CallType.CALL, + Router.CallType.CALL, ARBITRUM_USDC, abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), new uint256[](0), false ); - actions[1] = _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); + actions[1] = _action(Router.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); uint256[] memory outputAmountSplices = new uint256[](1); outputAmountSplices[0] = _splice(1, 0, 4, 32); actions[2] = _action( - DummyRouter.CallType.STATICCALL, + Router.CallType.STATICCALL, address(manipulator), abi.encodeCall( AcrossERC20AmountManipulator.deriveOutputAmount, (uint256(0), bridgeFee, uint256(18), uint256(18)) @@ -105,7 +105,7 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { ); actions[3] = _action( - DummyRouter.CallType.CALL, + Router.CallType.CALL, ARBITRUM_WETH, abi.encodeWithSelector(ERC20.approve.selector, ACROSS_ARBITRUM_SPOKE_POOL, type(uint256).max), new uint256[](0), @@ -117,12 +117,12 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { depositSplices[1] = _splice(2, 0, 164, 32); actions[4] = _action( - DummyRouter.CallType.CALL, ACROSS_ARBITRUM_SPOKE_POOL, _emptyAcrossDepositCalldata(), depositSplices, false + Router.CallType.CALL, ACROSS_ARBITRUM_SPOKE_POOL, _emptyAcrossDepositCalldata(), depositSplices, false ); } function _assertPocResult( - DummyRouter router, + Router router, uint256 bridgeFee, uint256 spokePoolWethBefore, bytes memory manipulatorResult @@ -136,10 +136,10 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { assertEq(actualOutputAmount, actualInputAmount - bridgeFee); } - function _routerAtFixtureAddress() internal returns (DummyRouter router) { - DummyRouter implementation = new DummyRouter(); + function _routerAtFixtureAddress() internal returns (Router router) { + Router implementation = new Router(address(this)); vm.etch(FIXTURE_ROUTER, address(implementation).code); - return DummyRouter(payable(FIXTURE_ROUTER)); + return Router(payable(FIXTURE_ROUTER)); } function _emptyAcrossDepositCalldata() internal view returns (bytes memory) { @@ -161,21 +161,16 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { } function _action( - DummyRouter.CallType callType, + Router.CallType callType, address target, bytes memory data, uint256[] memory splices, bool storeResult - ) internal pure returns (DummyRouter.Action memory) { - return - DummyRouter.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + ) internal pure returns (Router.Action memory) { + return Router.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); } - function _actionInfo(DummyRouter.CallType callType, address target, bool storeResult) - internal - pure - returns (uint256) - { + function _actionInfo(Router.CallType callType, address target, bool storeResult) internal pure returns (uint256) { return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); } diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index 60b140a..b653b75 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {DummyRouter} from "../../src/dummyRouter.sol"; +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; import {MathManipulator} from "../../src/manipulators/MathManipulator.sol"; interface IOpenOceanExchangeV2 { @@ -81,7 +81,7 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { vm.warp(FORK_BLOCK_TIMESTAMP); } - DummyRouter router = _routerAtFixtureAddress(); + Router router = _routerAtFixtureAddress(); MathManipulator manipulator = new MathManipulator(); if (bytes(rpcUrl).length == 0) { emit log("Set ARBITRUM_RPC to execute this fork PoC."); @@ -96,14 +96,14 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { uint256 initialFeeRecipientBalance = FEE_RECIPIENT.balance; uint256 initialWethBalance = ERC20(ARBITRUM_WETH).balanceOf(address(router)); - DummyRouter.Action[] memory actions = _buildActions( + Router.Action[] memory actions = _buildActions( manipulator, inputAmount, nativeFee, _openOceanSwapCalldata(inputAmount), _stargateCalldata(nativeFee) ); uint256 gasBeforeExecute = gasleft(); - bytes[] memory results = router.execute(actions); + bytes[] memory results = router.performModularExecution(actions); uint256 executeGasUsed = gasBeforeExecute - gasleft(); - emit log_named_uint("router.execute gas used", executeGasUsed); + emit log_named_uint("router.performModularExecution gas used", executeGasUsed); _assertPocResult( router, @@ -202,22 +202,22 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { uint256 nativeFee, bytes memory swapCalldata, bytes memory stargateCalldata - ) internal pure returns (DummyRouter.Action[] memory actions) { - actions = new DummyRouter.Action[](7); + ) internal pure returns (Router.Action[] memory actions) { + actions = new Router.Action[](7); actions[0] = _action( - DummyRouter.CallType.CALL, + Router.CallType.CALL, ARBITRUM_USDC, abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), new uint256[](0), false ); - actions[1] = _action(DummyRouter.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); + actions[1] = _action(Router.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); uint256[] memory feeSplices = new uint256[](1); feeSplices[0] = _splice(1, 0, 4, 32); actions[2] = _action( - DummyRouter.CallType.STATICCALL, + Router.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.percent, (uint256(0), ROUTE_FEE_BPS)), feeSplices, @@ -227,18 +227,14 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { uint256[] memory feeTransferSplices = new uint256[](1); feeTransferSplices[0] = _splice(2, 0, 0, 32); actions[3] = _action( - DummyRouter.CallType.CALL_WITH_NATIVE, - FEE_RECIPIENT, - abi.encodePacked(uint256(0)), - feeTransferSplices, - false + Router.CallType.CALL_WITH_NATIVE, FEE_RECIPIENT, abi.encodePacked(uint256(0)), feeTransferSplices, false ); uint256[] memory postFeeSplices = new uint256[](2); postFeeSplices[0] = _splice(1, 0, 4, 32); postFeeSplices[1] = _splice(2, 0, 36, 32); actions[4] = _action( - DummyRouter.CallType.STATICCALL, + Router.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.subtract, (uint256(0), uint256(0))), postFeeSplices, @@ -248,7 +244,7 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { uint256[] memory bridgeAmountSplices = new uint256[](1); bridgeAmountSplices[0] = _splice(4, 0, 4, 32); actions[5] = _action( - DummyRouter.CallType.STATICCALL, + Router.CallType.STATICCALL, address(manipulator), abi.encodeCall(MathManipulator.subtract, (uint256(0), nativeFee)), bridgeAmountSplices, @@ -259,7 +255,7 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { stargateSplices[0] = _splice(4, 0, 0, 32); stargateSplices[1] = _splice(5, 0, uint64(CALL_WITH_NATIVE_PAYLOAD_OFFSET + STARGATE_AMOUNT_OFFSET), 32); actions[6] = _action( - DummyRouter.CallType.CALL_WITH_NATIVE, + Router.CallType.CALL_WITH_NATIVE, STARGATE_NATIVE_WRAPPER, abi.encodePacked(uint256(0), stargateCalldata), stargateSplices, @@ -268,7 +264,7 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { } function _assertPocResult( - DummyRouter router, + Router router, uint256 nativeFee, uint256 initialNativeBalance, uint256 initialFeeRecipientBalance, @@ -293,28 +289,23 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { assertLt(address(router).balance - initialNativeBalance, nativeFee); } - function _routerAtFixtureAddress() internal returns (DummyRouter router) { - DummyRouter implementation = new DummyRouter(); + function _routerAtFixtureAddress() internal returns (Router router) { + Router implementation = new Router(address(this)); vm.etch(FIXTURE_ROUTER, address(implementation).code); - return DummyRouter(payable(FIXTURE_ROUTER)); + return Router(payable(FIXTURE_ROUTER)); } function _action( - DummyRouter.CallType callType, + Router.CallType callType, address target, bytes memory data, uint256[] memory splices, bool storeResult - ) internal pure returns (DummyRouter.Action memory) { - return - DummyRouter.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + ) internal pure returns (Router.Action memory) { + return Router.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); } - function _actionInfo(DummyRouter.CallType callType, address target, bool storeResult) - internal - pure - returns (uint256) - { + function _actionInfo(Router.CallType callType, address target, bool storeResult) internal pure returns (uint256) { return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); } From d228d5af614ce106a8775feec0a2567078922bdf Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 12 May 2026 19:30:39 +0400 Subject: [PATCH 17/69] feat: clean up --- src/dummyRouter.sol | 97 ------- src/swapFeeBridgeRouter.sol | 66 ----- ...StargateNativeSwapFeeBridgeRouterPoC.t.sol | 241 ------------------ 3 files changed, 404 deletions(-) delete mode 100644 src/dummyRouter.sol delete mode 100644 src/swapFeeBridgeRouter.sol delete mode 100644 test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol diff --git a/src/dummyRouter.sol b/src/dummyRouter.sol deleted file mode 100644 index 9218668..0000000 --- a/src/dummyRouter.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -contract DummyRouter { - enum CallType { - CALL, - STATICCALL, - CALL_WITH_NATIVE - } - - struct Action { - uint256 actionInfo; - bytes data; - uint256[] splices; - } - - error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); - error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); - error CallFailed(uint256 actionIndex, bytes returndata); - error MissingNativeValue(uint256 actionIndex); - - function execute(Action[] calldata actions) external payable returns (bytes[] memory results) { - uint256 actionsLength = actions.length; - results = new bytes[](actionsLength); - - for (uint256 i; i < actionsLength;) { - Action calldata action = actions[i]; - bytes memory callData = action.data; - - // Patch this action's calldata using earlier action results. - uint256 splicesLength = action.splices.length; - for (uint256 j; j < splicesLength;) { - uint256 spliceInfo = action.splices[j]; - uint256 sourceActionIndex = uint64(spliceInfo); - if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); - - uint256 srcOffset = uint64(spliceInfo >> 64); - uint256 dstOffset = uint64(spliceInfo >> 128); - uint256 length = spliceInfo >> 192; - bytes memory source = results[sourceActionIndex]; - if (srcOffset + length > source.length || dstOffset + length > callData.length) { - revert SpliceOutOfBounds(i, j); - } - - assembly ("memory-safe") { - mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) - } - - unchecked { - ++j; - } - } - - bool success; - uint256 actionInfo = action.actionInfo; - bool storeResult = (actionInfo & 0xff00) != 0; - uint256 callType = actionInfo & 0xff; - address target = address(uint160(actionInfo >> 16)); - - if (callType == uint256(CallType.STATICCALL)) { - assembly ("memory-safe") { - success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) - } - } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { - if (callData.length < 32) revert MissingNativeValue(i); - uint256 callValue; - uint256 payloadLength = callData.length - 32; - assembly ("memory-safe") { - callValue := mload(add(callData, 0x20)) - success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) - } - } else { - assembly ("memory-safe") { - success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) - } - } - - if (!success || storeResult) { - bytes memory ret; - assembly ("memory-safe") { - let returnDataSize := returndatasize() - ret := mload(0x40) - mstore(ret, returnDataSize) - returndatacopy(add(ret, 0x20), 0, returnDataSize) - mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) - } - if (!success) revert CallFailed(i, ret); - results[i] = ret; - } - unchecked { - ++i; - } - } - } - - receive() external payable {} -} diff --git a/src/swapFeeBridgeRouter.sol b/src/swapFeeBridgeRouter.sol deleted file mode 100644 index efe93ab..0000000 --- a/src/swapFeeBridgeRouter.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -contract SwapFeeBridgeRouter { - bytes4 internal constant APPROVE_SELECTOR = 0x095ea7b3; - uint256 internal constant BPS_DENOMINATOR = 10_000; - - struct SwapFeeBridgeParams { - address inputToken; - address approveTarget; - uint256 inputAmount; - address swapTarget; - bytes swapData; - address bridgeTarget; - bytes bridgeData; - uint256 bridgeAmountOffset; - address feeRecipient; - uint256 feeBps; - uint256 nativeFee; - } - - error ApproveFailed(bytes returndata); - error SwapFailed(bytes returndata); - error FeeTransferFailed(bytes returndata); - error BridgeCalldataOutOfBounds(uint256 offset, uint256 length); - error BridgeFailed(bytes returndata); - - function swapFeeBridge(SwapFeeBridgeParams calldata params) - external - payable - returns (uint256 swapOutput, uint256 routeFee, uint256 postFeeAmount, uint256 bridgeAmount) - { - bool success; - bytes memory returndata; - - (success, returndata) = - params.inputToken.call(abi.encodeWithSelector(APPROVE_SELECTOR, params.approveTarget, params.inputAmount)); - if (!success) revert ApproveFailed(returndata); - - (success, returndata) = params.swapTarget.call(params.swapData); - if (!success) revert SwapFailed(returndata); - - swapOutput = abi.decode(returndata, (uint256)); - routeFee = swapOutput * params.feeBps / BPS_DENOMINATOR; - postFeeAmount = swapOutput - routeFee; - bridgeAmount = postFeeAmount - params.nativeFee; - - (success, returndata) = params.feeRecipient.call{value: routeFee}(""); - if (!success) revert FeeTransferFailed(returndata); - - bytes memory bridgeData = params.bridgeData; - uint256 bridgeAmountOffset = params.bridgeAmountOffset; - if (bridgeData.length < 32 || bridgeAmountOffset > bridgeData.length - 32) { - revert BridgeCalldataOutOfBounds(bridgeAmountOffset, bridgeData.length); - } - - assembly ("memory-safe") { - mstore(add(add(bridgeData, 0x20), bridgeAmountOffset), bridgeAmount) - } - - (success, returndata) = params.bridgeTarget.call{value: postFeeAmount}(bridgeData); - if (!success) revert BridgeFailed(returndata); - } - - receive() external payable {} -} diff --git a/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol deleted file mode 100644 index 2043497..0000000 --- a/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol +++ /dev/null @@ -1,241 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity =0.8.25; - -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "solady/src/tokens/ERC20.sol"; - -import {SwapFeeBridgeRouter} from "../../src/swapFeeBridgeRouter.sol"; - -interface ISwapFeeBridgeOpenOceanExchangeV2 { - struct SwapDescription { - address srcToken; - address dstToken; - address srcReceiver; - address dstReceiver; - uint256 amount; - uint256 minReturnAmount; - uint256 flags; - address referrer; - bytes permit; - } - - struct CallDescription { - uint256 target; - uint256 gasLimit; - uint256 value; - bytes data; - } -} - -interface ISwapFeeBridgeStargateNative { - struct SendParam { - uint32 dstEid; - bytes32 to; - uint256 amountLD; - uint256 minAmountLD; - bytes extraOptions; - bytes composeMsg; - bytes oftCmd; - } - - struct MessagingFee { - uint256 nativeFee; - uint256 lzTokenFee; - } - - function send(SendParam calldata sendParam, MessagingFee calldata fee, address refundAddress) external payable; -} - -// ref tx 0xef65dc3323cd757c5e3a1a872b99beff6e71f0a80b1a2a6d280d2f2458f3cbaf -contract OpenOceanStargateNativeSwapFeeBridgeRouterPoCTest is Test { - bytes4 internal constant OPENOCEAN_SWAP_SELECTOR = 0x0a9704d5; - address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; - address internal constant OPENOCEAN_CALLER = 0xB100a5B2591Dd099040a5ab76EFe682A6D8a48a2; - address internal constant OPENOCEAN_REFERRER = 0x38c7720238a2C123814aaF1A3D0e31E0093aF046; - address internal constant STARGATE_NATIVE_WRAPPER = 0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F; - address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; - address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; - address internal constant FEE_RECIPIENT = 0x0079a23EDEA601190EdF1cda05c8Af3fEA2f2d9F; - address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; - address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; - address internal constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - uint256 internal constant FORK_BLOCK_NUMBER = 461_745_499; - uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01f38b; - uint256 internal constant SWAP_INPUT_USDC = 0x1312d00; - uint256 internal constant OPENOCEAN_MIN_RETURN = 0x1b91a33e163bdf; - uint256 internal constant OPENOCEAN_FLAGS = 2; - uint256 internal constant STARGATE_NATIVE_FEE = 0x1603e90a5fe0; - uint256 internal constant ROUTE_FEE_BPS = 100; - - uint32 internal constant BASE_ENDPOINT_ID = 30_184; - uint256 internal constant STARGATE_AMOUNT_OFFSET = 196; - - function test_openOceanSwapFeeBridgeRouter_arbitrumFork() public { - string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); - if (bytes(rpcUrl).length != 0) { - uint256 forkBlock = vm.envOr("ARBITRUM_FORK_BLOCK", FORK_BLOCK_NUMBER); - vm.createSelectFork(rpcUrl, forkBlock); - vm.warp(FORK_BLOCK_TIMESTAMP); - } - - SwapFeeBridgeRouter router = _routerAtFixtureAddress(); - if (bytes(rpcUrl).length == 0) { - emit log("Set ARBITRUM_RPC to execute this fork PoC."); - return; - } - - uint256 inputAmount = vm.envOr("POC_USDC_AMOUNT", SWAP_INPUT_USDC); - uint256 nativeFee = vm.envOr("POC_STARGATE_NATIVE_FEE", STARGATE_NATIVE_FEE); - - deal(ARBITRUM_USDC, address(router), inputAmount); - uint256 initialNativeBalance = address(router).balance; - uint256 initialFeeRecipientBalance = FEE_RECIPIENT.balance; - uint256 initialWethBalance = ERC20(ARBITRUM_WETH).balanceOf(address(router)); - - SwapFeeBridgeRouter.SwapFeeBridgeParams memory params = SwapFeeBridgeRouter.SwapFeeBridgeParams({ - inputToken: ARBITRUM_USDC, - approveTarget: OPENOCEAN_EXCHANGE_V2, - inputAmount: inputAmount, - swapTarget: OPENOCEAN_EXCHANGE_V2, - swapData: _openOceanSwapCalldata(inputAmount), - bridgeTarget: STARGATE_NATIVE_WRAPPER, - bridgeData: _stargateCalldata(nativeFee), - bridgeAmountOffset: STARGATE_AMOUNT_OFFSET, - feeRecipient: FEE_RECIPIENT, - feeBps: ROUTE_FEE_BPS, - nativeFee: nativeFee - }); - - uint256 gasBeforeSwapFeeBridge = gasleft(); - (uint256 swapOutput, uint256 routeFee, uint256 postFeeAmount, uint256 bridgeAmount) = - router.swapFeeBridge(params); - uint256 swapFeeBridgeGasUsed = gasBeforeSwapFeeBridge - gasleft(); - emit log_named_uint("router.swapFeeBridge gas used", swapFeeBridgeGasUsed); - - _assertPocResult( - router, - nativeFee, - initialNativeBalance, - initialFeeRecipientBalance, - initialWethBalance, - swapOutput, - routeFee, - postFeeAmount, - bridgeAmount - ); - } - - function _openOceanSwapCalldata(uint256 inputAmount) internal pure returns (bytes memory) { - return abi.encodeWithSelector( - OPENOCEAN_SWAP_SELECTOR, - OPENOCEAN_CALLER, - ISwapFeeBridgeOpenOceanExchangeV2.SwapDescription({ - srcToken: ARBITRUM_USDC, - dstToken: NATIVE_TOKEN, - srcReceiver: OPENOCEAN_CALLER, - dstReceiver: FIXTURE_ROUTER, - amount: inputAmount, - minReturnAmount: OPENOCEAN_MIN_RETURN, - flags: OPENOCEAN_FLAGS, - referrer: OPENOCEAN_REFERRER, - permit: "" - }), - _openOceanCalls() - ); - } - - function _stargateCalldata(uint256 nativeFee) internal pure returns (bytes memory) { - return abi.encodeCall( - ISwapFeeBridgeStargateNative.send, - ( - ISwapFeeBridgeStargateNative.SendParam({ - dstEid: BASE_ENDPOINT_ID, - to: _toBytes32(FIXTURE_RECIPIENT), - amountLD: 0, - minAmountLD: 0, - extraOptions: "", - composeMsg: "", - oftCmd: "" - }), - ISwapFeeBridgeStargateNative.MessagingFee({nativeFee: nativeFee, lzTokenFee: 0}), - FIXTURE_RECIPIENT - ) - ); - } - - function _openOceanCalls() - internal - pure - returns (ISwapFeeBridgeOpenOceanExchangeV2.CallDescription[] memory calls) - { - calls = new ISwapFeeBridgeOpenOceanExchangeV2.CallDescription[](6); - calls[0] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ - target: 0, - gasLimit: 0, - value: 0, - data: hex"e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b9470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112a880000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab1000003000000000000000000000000000000000000" - }); - calls[1] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ - target: 0, - gasLimit: 0, - value: 0, - data: hex"9f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af9508000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" - }); - calls[2] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ - target: uint256(uint160(0xb7236B927e03542AC3bE0A054F2bEa8868AF9508)), - gasLimit: 0, - value: 0, - data: hex"53c059a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2" - }); - calls[3] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ - target: 0, - gasLimit: 0, - value: 0, - data: hex"9f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000400000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" - }); - calls[4] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ - target: 0, - gasLimit: 0, - value: 0, - data: hex"8a6a1e85000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000000000000000000000000000000000000000000000000001ea1d1d3352615" - }); - calls[5] = ISwapFeeBridgeOpenOceanExchangeV2.CallDescription({ - target: 0, - gasLimit: 0, - value: 0, - data: hex"9f865422000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" - }); - } - - function _assertPocResult( - SwapFeeBridgeRouter router, - uint256 nativeFee, - uint256 initialNativeBalance, - uint256 initialFeeRecipientBalance, - uint256 initialWethBalance, - uint256 swapOutput, - uint256 routeFee, - uint256 postFeeAmount, - uint256 bridgeAmount - ) internal view { - assertGt(swapOutput, 0); - assertEq(routeFee, swapOutput * ROUTE_FEE_BPS / 10_000); - assertEq(FEE_RECIPIENT.balance - initialFeeRecipientBalance, routeFee); - assertEq(postFeeAmount + routeFee, swapOutput); - assertEq(bridgeAmount + nativeFee, postFeeAmount); - assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); - assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), initialWethBalance); - assertLt(address(router).balance - initialNativeBalance, nativeFee); - } - - function _routerAtFixtureAddress() internal returns (SwapFeeBridgeRouter router) { - SwapFeeBridgeRouter implementation = new SwapFeeBridgeRouter(); - vm.etch(FIXTURE_ROUTER, address(implementation).code); - return SwapFeeBridgeRouter(payable(FIXTURE_ROUTER)); - } - - function _toBytes32(address addr) internal pure returns (bytes32) { - return bytes32(uint256(uint160(addr))); - } -} From ac7593365c65b487f4a720994a101356678ef509 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 13 May 2026 00:11:53 +0530 Subject: [PATCH 18/69] fix: stargate script --- scripts/e2e/config.ts | 2 +- scripts/e2e/swapBridgeViaStargateNative.ts | 45 ++++++++++++++-------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 6906384..d77d3d9 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -19,7 +19,7 @@ export const CHAIN_IDS = { export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; /** Deployed combined unchecked router instance (set via env after deployment) */ -export const ROUTER_ADDRESS: string = '0x98381Fb4dC5c2046558236857181F4e34a9088dC'; +export const ROUTER_ADDRESS: string = '0x33cBEF62f74f5204651D4C5Dcc3fd8E56A01F2aF'; /** Standard ERC-20 "native" sentinel used by CurrencyLib */ export const NATIVE_TOKEN_ADDRESS = diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index f30a444..b8e5bf0 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -221,12 +221,11 @@ function buildStargateCalldata( * pull USDC → swap USDC→ETH (OO) → post-fee ETH to signer → Stargate send * * Bridge design: - * - amountLD is pre-encoded in stargateData (= estimatedFinalAmount - nativeFee). - * - useFinalAmountAsValue=true forwards the actual post-fee ETH as msg.value. - * - The caller provides nativeFee as msg.value in the AH.exec call so the router's - * ETH balance at bridge time = actualFinalAmount + nativeFee, satisfying - * msg.value >= amountLD + nativeFee. - * - Any excess ETH refunded to the signer by Stargate via refundAddress. + * - amountLD is pre-encoded in stargateData as (minAmountOut - feeAmount - nativeFeeWithBuffer). + * - useFinalAmountAsValue=true forwards the actual post-fee ETH (finalAmount) as msg.value. + * - Because finalAmount >= minAmountOut - feeAmount = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee, + * the Stargate msg.value check always passes regardless of swap slippage. + * - Any excess ETH (finalAmount - amountLD - nativeFee) is refunded by Stargate to refundAddress. * * @param signerAddress Signer/recipient address * @param inputAmount USDC amount in base units @@ -336,7 +335,7 @@ async function main() { throw new Error('PRIVATE_KEY env var required'); } - const useModular = true; + const useModular = false; const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); @@ -395,22 +394,38 @@ async function main() { const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; // amountLD to encode in the send() calldata. - // Pre-encoded as (estimatedFinalAmount - nativeFeeWithBuffer) so that - // Stargate's msg.value check (msg.value >= amountLD + nativeFee) passes even - // at the worst-case estimated output. Any excess (actual > estimated) refunds - // to the signer via Stargate's refundAddress mechanism. - const amountLD = estimatedFinalAmount - nativeFeeWithBuffer; + // + // Stargate requires msg.value >= amountLD + nativeFee. With + // useFinalAmountAsValue=true, msg.value = finalAmount = actualSwapOut - feeAmount. + // + // To guarantee this holds even under maximum OO slippage we base amountLD on + // minAmountOut (OO's slippage floor) rather than estimatedOut: + // + // amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer + // + // Because feeAmount is a fixed pre-encoded value and feeAmount <= estimatedOut, + // we know actualSwapOut >= minAmountOut, so: + // finalAmount = actualSwapOut - feeAmount >= minAmountOut - feeAmount + // = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee ✓ + // + // Any excess ETH (finalAmount - amountLD - nativeFee) is refunded to the signer + // by Stargate via refundAddress. + // + // Using estimatedFinalAmount instead here will fail when slippage causes + // finalAmount < estimatedFinalAmount (Stargate_InvalidAmount 0x3442dd95). + const minFinalAmount = minAmountOut - feeAmount; + const amountLD = minFinalAmount - nativeFeeWithBuffer; if (amountLD <= 0n) { throw new Error( - `Estimated final ETH amount (${ethers.formatEther(estimatedFinalAmount)}) ` + - `is too small to cover the Stargate nativeFee (${ethers.formatEther(nativeFeeWithBuffer)}). ` + + `minAmountOut (${ethers.formatEther(minAmountOut)}) is too small to cover ` + + `feeAmount (${ethers.formatEther(feeAmount)}) + nativeFee (${ethers.formatEther(nativeFeeWithBuffer)}). ` + 'Increase your USDC balance.', ); } console.log(`Stargate nativeFee: ${ethers.formatEther(nativeFee)} ETH`); console.log(`nativeFee (+5% buf): ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); - console.log(`amountLD (encoded): ${ethers.formatEther(amountLD)} ETH`); + console.log(`amountLD (encoded): ${ethers.formatEther(amountLD)} ETH ← based on minAmountOut; excess refunded on-chain`); console.log(`Est. received Base: ${ethers.formatEther(amountReceivedLD)} ETH`); console.log(''); From 8b8e2062930ae1764db3275edcb10c507d47d5c8 Mon Sep 17 00:00:00 2001 From: arthcp Date: Wed, 13 May 2026 15:14:50 +0400 Subject: [PATCH 19/69] feat: polygon swap and bridge compare test --- test/poc/OneInchCctpOpenRouterPoC.t.sol | 298 ++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 test/poc/OneInchCctpOpenRouterPoC.t.sol diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol new file mode 100644 index 0000000..5822e8a --- /dev/null +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; +import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; + +interface ITokenMessengerV2 { + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold + ) external; +} + +// ref tx 0x3ce8e42b6a0b1f8dbc2f2872deb4c74f3b24d6814b2466134129ead30c2ca1de +contract OneInchCctpOpenRouterPoCTest is Test { + address internal constant ONEINCH_SWAP_TARGET = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant CCTP_TOKEN_MESSENGER_V2 = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant FEE_RECIPIENT = 0xc91E5068968ACAEC9C8E7C056390d9e3CB34f7FC; + address internal constant POLYGON_AAVE = 0xD6DF932A45C0f255f85145f286eA0b292B21C90B; + address internal constant POLYGON_USDC = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; + + uint256 internal constant FORK_BLOCK_NUMBER = 86_816_149; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a0451c3; + uint256 internal constant SWAP_INPUT_AAVE = 0x2d169fe80174000; + uint256 internal constant EXPECTED_SWAP_OUTPUT_USDC = 0x132b02c; + uint256 internal constant ROUTE_FEE_USDC = 0x7530; + uint256 internal constant EXPECTED_CCTP_BURN_AMOUNT = 0x1323afc; + uint32 internal constant BASE_CCTP_DOMAIN = 6; + uint256 internal constant CCTP_MAX_FEE = 0x2710; + uint32 internal constant CCTP_MIN_FINALITY_THRESHOLD = 1000; + string internal constant ONEINCH_SWAP_CALLDATA = + "0x90411a320000000000000000000000001e82ad8a12068a85fcb96368463b434e77b21201000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000001e82ad8a12068a85fcb96368463b434e77b212010000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000002d169fe801740000000000000000000000000000000000000000000000000000000000001140299000000000000000000000000000000000000000000000000000000000132b02c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000008a00000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000da00000000000000000000000000000000000000000000000000000000000000ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064eb5625d9000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba300000000000000000000000000000000000000000000000002d169fe8017400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008487517c45000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000001095692a6237d83c6a72f3f5efedb9a670c4922300000000000000000000000000000000000000000000000002d169fe801740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001095692a6237d83c6a72f3f5efedb9a670c4922300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004a424856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003070b0e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000002d169fe8017400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e82ad8a12068a85fcb96368463b434e77b21201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001449f86542200000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000100000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002449f8654220000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf127000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb000000000000000000000000b6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000001e82ad8a12068a85fcb96368463b434e77b2120100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002e0d500b1d8e8ef31e21c99d1db9a6444d3adf12700001f43c499c542cef5e3811e1192ce70d8cc03d5c33590000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c3359000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000300000000000000000000000000000000000000000000000000000132c7bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f8654220000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f990000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + function test_oneInchSwapCctpBridge_polygonFork() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + Router router = _routerAtFixtureAddress(); + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_AAVE_AMOUNT", SWAP_INPUT_AAVE); + + deal(POLYGON_AAVE, FIXTURE_RECIPIENT, inputAmount); + deal(POLYGON_AAVE, address(router), 0); + deal(POLYGON_USDC, address(router), 0); + + vm.prank(FIXTURE_RECIPIENT); + ERC20(POLYGON_AAVE).approve(address(ALLOWANCE_HOLDER), inputAmount); + + uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); + uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); + + Router.Action[] memory actions = _buildActions(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); + + bytes memory ahResult; + uint256 gasBeforeExecute = gasleft(); + vm.prank(FIXTURE_RECIPIENT); + ahResult = IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( + address(router), + POLYGON_AAVE, + inputAmount, + payable(address(router)), + abi.encodeCall(router.performModularExecution, (actions)) + ); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("AllowanceHolder.exec -> router.performModularExecution gas used", executeGasUsed); + + bytes[] memory results = abi.decode(ahResult, (bytes[])); + _assertPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore, results[2], results[5]); + } + + function test_oneInchSwapCctpBridgeMonolithic_polygonFork() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + Router router = _routerAtFixtureAddress(); + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_AAVE_AMOUNT", SWAP_INPUT_AAVE); + + deal(POLYGON_AAVE, FIXTURE_RECIPIENT, inputAmount); + deal(POLYGON_AAVE, address(router), 0); + deal(POLYGON_USDC, address(router), 0); + + vm.prank(FIXTURE_RECIPIENT); + ERC20(POLYGON_AAVE).approve(address(ALLOWANCE_HOLDER), inputAmount); + + uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); + uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); + + Router.MonolithicExecution memory exec = + _buildMonolithicExecution(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); + + uint256 gasBeforeExecute = gasleft(); + vm.prank(FIXTURE_RECIPIENT); + IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( + address(router), + POLYGON_AAVE, + inputAmount, + payable(address(router)), + abi.encodeCall(router.performExecution, (exec)) + ); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("AllowanceHolder.exec -> router.performExecution gas used", executeGasUsed); + + _assertMonolithicPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); + } + + function _buildActions(uint256 inputAmount, bytes memory swapCalldata) + internal + pure + returns (Router.Action[] memory actions) + { + actions = new Router.Action[](7); + actions[0] = _action( + Router.CallType.CALL, + address(ALLOWANCE_HOLDER), + abi.encodeWithSelector( + IAllowanceHolder.transferFrom.selector, POLYGON_AAVE, FIXTURE_RECIPIENT, FIXTURE_ROUTER, inputAmount + ), + new uint256[](0), + false + ); + + actions[1] = _action( + Router.CallType.CALL, + POLYGON_AAVE, + abi.encodeWithSelector(ERC20.approve.selector, ONEINCH_SWAP_TARGET, inputAmount), + new uint256[](0), + false + ); + + actions[2] = _action(Router.CallType.CALL, ONEINCH_SWAP_TARGET, swapCalldata, new uint256[](0), true); + + actions[3] = _action( + Router.CallType.CALL, + POLYGON_USDC, + abi.encodeWithSelector(ERC20.transfer.selector, FEE_RECIPIENT, ROUTE_FEE_USDC), + new uint256[](0), + false + ); + + actions[4] = _action( + Router.CallType.CALL, + POLYGON_USDC, + abi.encodeWithSelector(ERC20.approve.selector, CCTP_TOKEN_MESSENGER_V2, type(uint256).max), + new uint256[](0), + false + ); + + actions[5] = _action( + Router.CallType.STATICCALL, + POLYGON_USDC, + abi.encodeWithSelector(ERC20.balanceOf.selector, FIXTURE_ROUTER), + new uint256[](0), + true + ); + + uint256[] memory depositSplices = new uint256[](1); + depositSplices[0] = _splice(5, 0, 4, 32); + actions[6] = _action( + Router.CallType.CALL, CCTP_TOKEN_MESSENGER_V2, _emptyDepositForBurnCalldata(), depositSplices, false + ); + } + + function _buildMonolithicExecution(uint256 inputAmount, bytes memory swapCalldata) + internal + pure + returns (Router.MonolithicExecution memory exec) + { + uint256[] memory amountPositions = new uint256[](1); + amountPositions[0] = 4; + + exec = Router.MonolithicExecution({ + input: Router.InputData({user: FIXTURE_RECIPIENT, inputToken: POLYGON_AAVE, inputAmount: inputAmount}), + preFee: Router.FeeData({receiver: address(0), amount: 0}), + swap: Router.SwapData({ + target: ONEINCH_SWAP_TARGET, + approvalSpender: ONEINCH_SWAP_TARGET, + outputToken: POLYGON_USDC, + value: 0, + minOutput: EXPECTED_SWAP_OUTPUT_USDC, + data: swapCalldata + }), + postFee: Router.FeeData({receiver: FEE_RECIPIENT, amount: ROUTE_FEE_USDC}), + bridge: Router.BridgeData({ + target: CCTP_TOKEN_MESSENGER_V2, + approvalSpender: CCTP_TOKEN_MESSENGER_V2, + value: 0, + data: _emptyDepositForBurnCalldata(), + amountPositions: amountPositions, + useFinalAmountAsValue: false + }) + }); + } + + function _assertPocResult( + Router router, + uint256 feeRecipientUsdcBefore, + uint256 usdcSupplyBefore, + bytes memory oneInchResult, + bytes memory balanceResult + ) internal view { + uint256 swapOutput = abi.decode(oneInchResult, (uint256)); + uint256 bridgeAmount = abi.decode(balanceResult, (uint256)); + + assertEq(swapOutput, EXPECTED_SWAP_OUTPUT_USDC); + assertEq(bridgeAmount, EXPECTED_CCTP_BURN_AMOUNT); + assertEq(bridgeAmount, swapOutput - ROUTE_FEE_USDC); + assertEq(ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT) - feeRecipientUsdcBefore, ROUTE_FEE_USDC); + assertEq(ERC20(POLYGON_USDC).totalSupply(), usdcSupplyBefore - bridgeAmount); + assertEq(ERC20(POLYGON_AAVE).balanceOf(address(router)), 0); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0); + } + + function _assertMonolithicPocResult(Router router, uint256 feeRecipientUsdcBefore, uint256 usdcSupplyBefore) + internal + view + { + assertEq(ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT) - feeRecipientUsdcBefore, ROUTE_FEE_USDC); + assertEq(ERC20(POLYGON_USDC).totalSupply(), usdcSupplyBefore - EXPECTED_CCTP_BURN_AMOUNT); + assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_RECIPIENT), 0); + assertEq(ERC20(POLYGON_AAVE).balanceOf(address(router)), 0); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0); + } + + function _routerAtFixtureAddress() internal returns (Router router) { + Router implementation = new Router(address(this)); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return Router(payable(FIXTURE_ROUTER)); + } + + function _emptyDepositForBurnCalldata() internal pure returns (bytes memory) { + return abi.encodeCall( + ITokenMessengerV2.depositForBurn, + ( + uint256(0), + BASE_CCTP_DOMAIN, + _toBytes32(FIXTURE_RECIPIENT), + POLYGON_USDC, + bytes32(0), + CCTP_MAX_FEE, + CCTP_MIN_FINALITY_THRESHOLD + ) + ); + } + + function _action( + Router.CallType callType, + address target, + bytes memory data, + uint256[] memory splices, + bool storeResult + ) internal pure returns (Router.Action memory) { + return Router.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + } + + function _actionInfo(Router.CallType callType, address target, bool storeResult) internal pure returns (uint256) { + return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); + } + + function _splice(uint64 sourceActionIndex, uint64 srcOffset, uint64 dstOffset, uint64 length) + internal + pure + returns (uint256) + { + return uint256(sourceActionIndex) | (uint256(srcOffset) << 64) | (uint256(dstOffset) << 128) + | (uint256(length) << 192); + } + + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} From 57030ae2fdd77511e04220a37c1d1e2814d9f8c3 Mon Sep 17 00:00:00 2001 From: arthcp Date: Wed, 13 May 2026 15:59:56 +0400 Subject: [PATCH 20/69] feat: use returnDataWordOffset on monolithic --- scripts/e2e/swapBridgeViaArbitrumNative.ts | 5 +- scripts/e2e/swapBridgeViaCctp.ts | 5 +- scripts/e2e/swapBridgeViaStargateNative.ts | 1 + scripts/e2e/utils/contractTypes.ts | 2 + scripts/e2e/utils/routerAbi.ts | 2 +- src/combined/BungeeOpenRouterV2.sol | 66 ++++++++++++++------ src/combined/BungeeOpenRouterV2Unchecked.sol | 58 ++++++++++------- test/poc/OneInchCctpOpenRouterPoC.t.sol | 3 +- 8 files changed, 97 insertions(+), 45 deletions(-) diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index a6557ef..101b13d 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -9,7 +9,7 @@ * 0.001 ETH is used if estimation fails. * 3. Build a post-swap fee to signer in ETH. * 4. Build either monolithic or modular execution payload. - * - Monolithic: swap AAVE→ETH (balance delta on NATIVE), take ETH fee, + * - Monolithic: swap AAVE→ETH (decoded return amount), take ETH fee, * call Arbitrum inbox with useFinalAmountAsValue=true so finalAmount * becomes msg.value on the depositEth call. * - Modular: pull → approve(oo) → swap(oo) → send ETH fee via CALL_WITH_NATIVE → @@ -176,7 +176,7 @@ function buildDepositEthCalldata(): string { /** * Builds a MonolithicExecution that: * - Pulls inputAmount AAVE from user - * - Swaps AAVE → ETH via OpenOcean (balance delta on NATIVE_TOKEN_ADDRESS) + * - Swaps AAVE → ETH via OpenOcean (decoded return amount) * - Takes feeAmount ETH as post-swap fee sent to signer * - Calls Arbitrum inbox depositEth() with finalAmount as msg.value * (via useFinalAmountAsValue=true — no amount to splice in calldata) @@ -203,6 +203,7 @@ function buildMonolithicExecution( value: 0n, minOutput: minAmountOut, data: swapData, + returnDataWordOffset: 0n, }, postFee: { receiver: signerAddress, diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index 91ced86..ec3efbc 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -6,7 +6,7 @@ * 2. Build CCTP v2 depositForBurn calldata with a zero amount placeholder * at byte offset 4 (the first parameter). * 3. Build either a monolithic or modular execution payload. - * - Monolithic: swap inside the router using pre/post balance delta, + * - Monolithic: swap inside the router using the decoded swap return amount, * take a post-swap fee in USDC, splice finalAmount into depositForBurn, * approve TOKEN_MESSENGER, call TOKEN_MESSENGER. * - Modular: discrete actions — pull → approve(oo) → swap(oo) → transfer fee → @@ -153,7 +153,7 @@ function buildDepositForBurnCalldata( * Builds a MonolithicExecution that: * - Pulls inputAmount AAVE from user * - No pre-swap fee - * - Swaps AAVE → USDC via OpenOcean (balance delta) + * - Swaps AAVE → USDC via OpenOcean (decoded return amount) * - Takes feeAmount USDC as post-swap fee to signer * - Splices finalAmount into depositForBurn at offset 4 * - Approves TOKEN_MESSENGER and calls depositForBurn @@ -182,6 +182,7 @@ function buildMonolithicExecution( value: 0n, minOutput: minAmountOut, data: swapData, + returnDataWordOffset: 0n, }, postFee: { receiver: signerAddress, diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index b8e5bf0..86b9f66 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -258,6 +258,7 @@ function buildMonolithicExecution( value: 0n, minOutput: minAmountOut, data: swapData, + returnDataWordOffset: 0n, }, postFee: { receiver: signerAddress, diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index 1a8edda..aeaf23e 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -24,6 +24,7 @@ export interface SwapData { value: bigint; minOutput: bigint; data: string; + returnDataWordOffset: bigint; } export interface BridgeData { @@ -58,4 +59,5 @@ export const NO_SWAP: SwapData = { value: 0n, minOutput: 0n, data: '0x', + returnDataWordOffset: 0n, }; diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index ef694df..a64499f 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -8,7 +8,7 @@ export const ROUTER_ABI = [ ( (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) preFee, - (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, bytes data) swap, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, bytes data, uint256 returnDataWordOffset) swap, (address receiver, uint256 amount) postFee, (address target, address approvalSpender, uint256 value, bytes data, uint256[] amountPositions, bool useFinalAmountAsValue) bridge ) exec diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol index 2d7d635..67fe166 100644 --- a/src/combined/BungeeOpenRouterV2.sol +++ b/src/combined/BungeeOpenRouterV2.sol @@ -56,10 +56,11 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { struct SwapData { address target; address approvalSpender; // 0 to skip ERC20 approval before swap - address outputToken; // token measured via balance delta + address outputToken; // token used for post-fee transfer / bridge approval uint256 value; // ETH forwarded to the swap target - uint256 minOutput; // minimum balance delta; reverts if not met + uint256 minOutput; // minimum decoded output; reverts if not met bytes data; + uint256 returnDataWordOffset; } /// @notice Mandatory bridge call. `amountPositions` lists every byte offset @@ -127,6 +128,7 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); error CallFailed(uint256 actionIndex, bytes returndata); error MissingNativeValue(uint256 actionIndex); + error ReturnDataOutOfBounds(); // ========================================================================= // Constructor @@ -191,7 +193,7 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); } - // 3. optional swap, accounted via pre/post balance delta + // 3. optional swap, accounted via decoded returndata address finalToken; uint256 finalAmount; if (exec.swap.target != address(0)) { @@ -230,16 +232,14 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { // when useFinalAmountAsValue is set, forward finalAmount as msg.value so // native-token bridges (e.g. Arbitrum inbox) receive the exact bridged amount. uint256 bridgeValue = exec.bridge.useFinalAmountAsValue ? finalAmount : exec.bridge.value; - _performAction(exec.bridge.target, bridgeValue, bridgeData); + _doCall(exec.bridge.target, bridgeValue, bridgeData, false); } - /// @dev Balance-delta swap helper; split out to keep _runMonolithic under 100 lines. + /// @dev Swap helper; decodes final amount from a returndata word. function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { - uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 swapInput; unchecked { @@ -248,22 +248,14 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); } - _performAction(exec.swap.target, exec.swap.value, exec.swap.data); + bytes memory ret = _doCall(exec.swap.target, exec.swap.value, exec.swap.data, true); + finalAmount = _decodeReturnWord(ret, exec.swap.returnDataWordOffset); - uint256 postBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - if (postBalance < preBalance) { - revert SwapOutputInsufficient(); - } - uint256 delta; - unchecked { - delta = postBalance - preBalance; - } - if (delta < exec.swap.minOutput) { + if (finalAmount < exec.swap.minOutput) { revert SwapOutputInsufficient(); } finalToken = exec.swap.outputToken; - finalAmount = delta; } // ========================================================================= @@ -387,4 +379,42 @@ contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { } } } + + // ========================================================================= + // Internal: simple call dispatcher (used by monolithic path) + // ========================================================================= + + function _doCall(address target, uint256 value, bytes memory data, bool storeResult) + internal + returns (bytes memory ret) + { + bool success; + assembly ("memory-safe") { + success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) + } + + if (!success || storeResult) { + assembly ("memory-safe") { + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) + returndatacopy(add(ret, 0x20), 0, returnDataSize) + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) + } + if (!success) { + assembly ("memory-safe") { + revert(add(ret, 0x20), mload(ret)) + } + } + } + } + + function _decodeReturnWord(bytes memory ret, uint256 wordOffset) internal pure returns (uint256 word) { + uint256 offset = wordOffset * 32; + if (offset + 32 > ret.length) revert ReturnDataOutOfBounds(); + + assembly ("memory-safe") { + word := mload(add(add(ret, 0x20), offset)) + } + } } diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 3f25502..33674ea 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -54,6 +54,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { uint256 value; uint256 minOutput; bytes data; + uint256 returnDataWordOffset; } struct BridgeData { @@ -105,6 +106,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); error CallFailed(uint256 actionIndex, bytes returndata); error MissingNativeValue(uint256 actionIndex); + error ReturnDataOutOfBounds(); // ========================================================================= // Constructor @@ -159,7 +161,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); } - // 3. optional swap, accounted via pre/post balance delta + // 3. optional swap, accounted via decoded returndata address finalToken; uint256 finalAmount; if (exec.swap.target != address(0)) { @@ -198,16 +200,14 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // when useFinalAmountAsValue is set, forward finalAmount as msg.value so // native-token bridges (e.g. Arbitrum inbox) receive the exact bridged amount. uint256 bridgeValue = exec.bridge.useFinalAmountAsValue ? finalAmount : exec.bridge.value; - _doCall(exec.bridge.target, bridgeValue, bridgeData); + _doCall(exec.bridge.target, bridgeValue, bridgeData, false); } - /// @dev Balance-delta swap helper. + /// @dev Swap helper; decodes final amount from a returndata word. function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { - uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 swapInput; unchecked { @@ -216,22 +216,14 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); } - _doCall(exec.swap.target, exec.swap.value, exec.swap.data); + bytes memory ret = _doCall(exec.swap.target, exec.swap.value, exec.swap.data, true); + finalAmount = _decodeReturnWord(ret, exec.swap.returnDataWordOffset); - uint256 postBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - if (postBalance < preBalance) { - revert SwapOutputInsufficient(); - } - uint256 delta; - unchecked { - delta = postBalance - preBalance; - } - if (delta < exec.swap.minOutput) { + if (finalAmount < exec.swap.minOutput) { revert SwapOutputInsufficient(); } finalToken = exec.swap.outputToken; - finalAmount = delta; } // ========================================================================= @@ -360,13 +352,37 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Internal: simple call dispatcher (used by monolithic path) // ========================================================================= - function _doCall(address target, uint256 value, bytes memory data) internal returns (bytes memory ret) { - bool ok; - (ok, ret) = target.call{value: value}(data); - if (!ok) { + function _doCall(address target, uint256 value, bytes memory data, bool storeResult) + internal + returns (bytes memory ret) + { + bool success; + assembly ("memory-safe") { + success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) + } + + if (!success || storeResult) { assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) + returndatacopy(add(ret, 0x20), 0, returnDataSize) + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) } + if (!success) { + assembly ("memory-safe") { + revert(add(ret, 0x20), mload(ret)) + } + } + } + } + + function _decodeReturnWord(bytes memory ret, uint256 wordOffset) internal pure returns (uint256 word) { + uint256 offset = wordOffset * 32; + if (offset + 32 > ret.length) revert ReturnDataOutOfBounds(); + + assembly ("memory-safe") { + word := mload(add(add(ret, 0x20), offset)) } } } diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 5822e8a..3684e4e 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -204,7 +204,8 @@ contract OneInchCctpOpenRouterPoCTest is Test { outputToken: POLYGON_USDC, value: 0, minOutput: EXPECTED_SWAP_OUTPUT_USDC, - data: swapCalldata + data: swapCalldata, + returnDataWordOffset: 0 }), postFee: Router.FeeData({receiver: FEE_RECIPIENT, amount: ROUTE_FEE_USDC}), bridge: Router.BridgeData({ From c404e08c0f1234cfaa47dd865fecb15c2c789ad9 Mon Sep 17 00:00:00 2001 From: arthcp Date: Wed, 13 May 2026 16:15:00 +0400 Subject: [PATCH 21/69] feat: gateway test --- test/poc/OneInchCctpOpenRouterPoC.t.sol | 72 ++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 3684e4e..0c24dff 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -35,9 +35,16 @@ contract OneInchCctpOpenRouterPoCTest is Test { uint256 internal constant EXPECTED_SWAP_OUTPUT_USDC = 0x132b02c; uint256 internal constant ROUTE_FEE_USDC = 0x7530; uint256 internal constant EXPECTED_CCTP_BURN_AMOUNT = 0x1323afc; + uint256 internal constant REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE = 0x564150ddc57; + uint256 internal constant REFERENCE_GATEWAY_INITIAL_AAVE_ALLOWANCE = + SWAP_INPUT_AAVE + REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE; uint32 internal constant BASE_CCTP_DOMAIN = 6; uint256 internal constant CCTP_MAX_FEE = 0x2710; uint32 internal constant CCTP_MIN_FINALITY_THRESHOLD = 1000; + string internal constant SOCKET_GATEWAY_REFERENCE_CALLDATA_PREFIX = + "0x000001ad4db9cf6a00000000000000000000000000000000000000000000000000000000000001a60000000000000000000000000000000000000000000000000000000000000120000000000000000000000000b0bbff6311b7f245761A7846d3Ce7B1b100C1836000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000021050000000000000000000000000000000000000000000000000000000000007530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000013e4ee8f0b86000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000000000000000000000002d169fe80174000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000001304"; + string internal constant SOCKET_GATEWAY_REFERENCE_CALLDATA_SUFFIX = + "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; string internal constant ONEINCH_SWAP_CALLDATA = "0x90411a320000000000000000000000001e82ad8a12068a85fcb96368463b434e77b21201000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000001e82ad8a12068a85fcb96368463b434e77b212010000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000002d169fe801740000000000000000000000000000000000000000000000000000000000001140299000000000000000000000000000000000000000000000000000000000132b02c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000008a00000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000da00000000000000000000000000000000000000000000000000000000000000ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064eb5625d9000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba300000000000000000000000000000000000000000000000002d169fe8017400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008487517c45000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000001095692a6237d83c6a72f3f5efedb9a670c4922300000000000000000000000000000000000000000000000002d169fe801740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001095692a6237d83c6a72f3f5efedb9a670c4922300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004a424856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003070b0e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000002d169fe8017400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e82ad8a12068a85fcb96368463b434e77b21201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001449f86542200000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000100000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002449f8654220000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf127000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb000000000000000000000000b6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000001e82ad8a12068a85fcb96368463b434e77b2120100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002e0d500b1d8e8ef31e21c99d1db9a6444d3adf12700001f43c499c542cef5e3811e1192ce70d8cc03d5c33590000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c3359000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000300000000000000000000000000000000000000000000000000000132c7bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f8654220000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f990000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; @@ -70,8 +77,8 @@ contract OneInchCctpOpenRouterPoCTest is Test { Router.Action[] memory actions = _buildActions(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); bytes memory ahResult; - uint256 gasBeforeExecute = gasleft(); vm.prank(FIXTURE_RECIPIENT); + uint256 gasBeforeExecute = gasleft(); ahResult = IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( address(router), POLYGON_AAVE, @@ -115,8 +122,8 @@ contract OneInchCctpOpenRouterPoCTest is Test { Router.MonolithicExecution memory exec = _buildMonolithicExecution(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); - uint256 gasBeforeExecute = gasleft(); vm.prank(FIXTURE_RECIPIENT); + uint256 gasBeforeExecute = gasleft(); IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( address(router), POLYGON_AAVE, @@ -130,6 +137,48 @@ contract OneInchCctpOpenRouterPoCTest is Test { _assertMonolithicPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); } + function test_oneInchSwapCctpBridgeSocketGatewayReference_polygonFork() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_AAVE_AMOUNT", SWAP_INPUT_AAVE); + assertEq(inputAmount, SWAP_INPUT_AAVE, "reference gateway calldata fixes the input amount"); + + deal(POLYGON_AAVE, FIXTURE_RECIPIENT, inputAmount); + deal(POLYGON_AAVE, FIXTURE_ROUTER, 0); + deal(POLYGON_USDC, FIXTURE_ROUTER, 0); + + vm.prank(FIXTURE_RECIPIENT); + ERC20(POLYGON_AAVE).approve(FIXTURE_ROUTER, REFERENCE_GATEWAY_INITIAL_AAVE_ALLOWANCE); + + uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); + uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); + + bytes memory referenceCalldata = _referenceSocketGatewayCalldata(); + + vm.prank(FIXTURE_RECIPIENT, FIXTURE_RECIPIENT); + uint256 gasBeforeExecute = gasleft(); + (bool success, bytes memory returndata) = FIXTURE_ROUTER.call(referenceCalldata); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + if (!success) { + assembly ("memory-safe") { + revert(add(returndata, 0x20), mload(returndata)) + } + } + emit log_named_uint("SocketGateway reference call gas used", executeGasUsed); + + _assertSocketGatewayPocResult(feeRecipientUsdcBefore, usdcSupplyBefore); + } + function _buildActions(uint256 inputAmount, bytes memory swapCalldata) internal pure @@ -249,6 +298,17 @@ contract OneInchCctpOpenRouterPoCTest is Test { assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0); } + function _assertSocketGatewayPocResult(uint256 feeRecipientUsdcBefore, uint256 usdcSupplyBefore) internal view { + assertEq( + ERC20(POLYGON_AAVE).allowance(FIXTURE_RECIPIENT, FIXTURE_ROUTER), REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE + ); + assertEq(ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT) - feeRecipientUsdcBefore, ROUTE_FEE_USDC); + assertEq(ERC20(POLYGON_USDC).totalSupply(), usdcSupplyBefore - EXPECTED_CCTP_BURN_AMOUNT); + assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_RECIPIENT), 0); + assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_ROUTER), 0); + assertEq(ERC20(POLYGON_USDC).balanceOf(FIXTURE_ROUTER), 0); + } + function _routerAtFixtureAddress() internal returns (Router router) { Router implementation = new Router(address(this)); vm.etch(FIXTURE_ROUTER, address(implementation).code); @@ -270,6 +330,14 @@ contract OneInchCctpOpenRouterPoCTest is Test { ); } + function _referenceSocketGatewayCalldata() internal pure returns (bytes memory) { + return bytes.concat( + vm.parseBytes(SOCKET_GATEWAY_REFERENCE_CALLDATA_PREFIX), + vm.parseBytes(ONEINCH_SWAP_CALLDATA), + vm.parseBytes(SOCKET_GATEWAY_REFERENCE_CALLDATA_SUFFIX) + ); + } + function _action( Router.CallType callType, address target, From 61a3419471e73f3a6378d95504ad0873aae1429a Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 14 May 2026 19:32:27 +0530 Subject: [PATCH 22/69] feat: improve test scripts - txn log summary - changed most scripts src chain to Polygon for better gas comparison - added three cases in stargate native - added new usdt0 oft tests --- hardhat.config.ts | 267 ++- package.json | 3 + scripts/e2e/bridgeViaRelay.ts | 538 +++-- scripts/e2e/config.ts | 116 +- scripts/e2e/swapBridgeViaArbitrumNative.ts | 21 +- scripts/e2e/swapBridgeViaCctp.ts | 491 +++-- scripts/e2e/swapBridgeViaOft.ts | 785 +++++++ scripts/e2e/swapBridgeViaStargateNative.ts | 841 +++++--- scripts/e2e/utils/relayLinkQuote.ts | 88 + scripts/e2e/utils/sleep.ts | 5 + scripts/e2e/utils/txnLogSummary.ts | 24 + yarn.lock | 2139 ++++++++++++++++++++ 12 files changed, 4522 insertions(+), 796 deletions(-) create mode 100644 scripts/e2e/swapBridgeViaOft.ts create mode 100644 scripts/e2e/utils/relayLinkQuote.ts create mode 100644 scripts/e2e/utils/sleep.ts create mode 100644 scripts/e2e/utils/txnLogSummary.ts create mode 100644 yarn.lock diff --git a/hardhat.config.ts b/hardhat.config.ts index 3b2bd20..1fbbb90 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,23 +1,23 @@ -import "@nomicfoundation/hardhat-foundry"; -import "@nomicfoundation/hardhat-toolbox"; -import { config as dotenvConfig } from "dotenv"; -import { HardhatUserConfig } from "hardhat/config"; -import { resolve } from "path"; +import '@nomicfoundation/hardhat-foundry'; +import '@nomicfoundation/hardhat-toolbox'; +import { config as dotenvConfig } from 'dotenv'; +import { HardhatUserConfig } from 'hardhat/config'; +import { resolve } from 'path'; -dotenvConfig({ path: resolve(__dirname, "./.env") }); +dotenvConfig({ path: resolve(__dirname, './.env') }); const deployerKey = process.env.DEPLOYER_PRIVATE_KEY; const accounts = deployerKey ? [deployerKey] : []; const config: HardhatUserConfig = { solidity: { - version: "0.8.25", + version: '0.8.25', settings: { optimizer: { enabled: true, runs: 2000, }, - evmVersion: "cancun", + evmVersion: 'cancun', }, }, networks: { @@ -25,113 +25,118 @@ const config: HardhatUserConfig = { allowUnlimitedContractSize: true, }, ethereum: { - url: process.env.ETHEREUM_RPC ?? "https://eth.llamarpc.com", + url: process.env.ETHEREUM_RPC ?? 'https://eth.llamarpc.com', chainId: 1, accounts, }, polygon: { - url: process.env.POLYGON_RPC ?? "https://polygon.llamarpc.com", + url: process.env.POLYGON_RPC ?? 'https://polygon.llamarpc.com', chainId: 137, accounts, }, arbitrum: { - url: process.env.ARBITRUM_RPC ?? "https://rpc.ankr.com/arbitrum", + url: process.env.ARBITRUM_RPC ?? 'https://rpc.ankr.com/arbitrum', chainId: 42161, accounts, }, optimism: { - url: process.env.OPTIMISM_RPC ?? "https://mainnet.optimism.io", + url: process.env.OPTIMISM_RPC ?? 'https://mainnet.optimism.io', chainId: 10, accounts, }, base: { - url: process.env.BASE_RPC ?? "https://mainnet.base.org", + url: process.env.BASE_RPC ?? 'https://mainnet.base.org', chainId: 8453, accounts, }, avalanche: { - url: process.env.AVALANCHE_RPC ?? "https://rpc.ankr.com/avalanche", + url: process.env.AVALANCHE_RPC ?? 'https://rpc.ankr.com/avalanche', chainId: 43114, accounts, }, bsc: { - url: process.env.BSC_RPC ?? "https://bsc-dataseed.binance.org/", + url: process.env.BSC_RPC ?? 'https://bsc-dataseed.binance.org/', chainId: 56, accounts, }, linea: { - url: process.env.LINEA_RPC ?? "https://rpc.linea.build", + url: process.env.LINEA_RPC ?? 'https://rpc.linea.build', chainId: 59144, accounts, }, scroll: { - url: process.env.SCROLL_RPC ?? "https://1rpc.io/scroll", + url: process.env.SCROLL_RPC ?? 'https://1rpc.io/scroll', chainId: 534352, accounts, }, blast: { - url: process.env.BLAST_RPC ?? "https://blastl2-mainnet.public.blastapi.io", + url: + process.env.BLAST_RPC ?? 'https://blastl2-mainnet.public.blastapi.io', chainId: 81457, accounts, }, mode: { - url: process.env.MODE_RPC ?? "https://1rpc.io/mode", + url: process.env.MODE_RPC ?? 'https://1rpc.io/mode', chainId: 34443, accounts, }, mantle: { - url: process.env.MANTLE_RPC ?? "https://rpc.mantle.xyz", + url: process.env.MANTLE_RPC ?? 'https://rpc.mantle.xyz', chainId: 5000, accounts, }, gnosis: { - url: process.env.GNOSIS_RPC ?? "https://rpc.ankr.com/gnosis", + url: process.env.GNOSIS_RPC ?? 'https://rpc.ankr.com/gnosis', chainId: 100, accounts, }, sonic: { - url: process.env.SONIC_RPC ?? "https://rpc.ankr.com/sonic_mainnet", + url: process.env.SONIC_RPC ?? 'https://rpc.ankr.com/sonic_mainnet', chainId: 146, accounts, }, unichain: { - url: process.env.UNICHAIN_RPC ?? "https://0xrpc.io/uni", + url: process.env.UNICHAIN_RPC ?? 'https://0xrpc.io/uni', chainId: 130, accounts, }, berachain: { - url: process.env.BERACHAIN_RPC ?? "https://berachain-rpc.publicnode.com", + url: process.env.BERACHAIN_RPC ?? 'https://berachain-rpc.publicnode.com', chainId: 80094, accounts, }, ink: { - url: process.env.INK_RPC ?? "https://rpc-gel.inkonchain.com", + url: process.env.INK_RPC ?? 'https://rpc-gel.inkonchain.com', chainId: 57073, accounts, }, soneium: { - url: process.env.SONEIUM_RPC ?? "https://soneium.drpc.org", + url: process.env.SONEIUM_RPC ?? 'https://soneium.drpc.org', chainId: 1868, accounts, }, worldchain: { - url: process.env.WORLDCHAIN_RPC ?? "https://worldchain-mainnet.g.alchemy.com/public", + url: + process.env.WORLDCHAIN_RPC ?? + 'https://worldchain-mainnet.g.alchemy.com/public', chainId: 480, accounts, }, sei: { - url: process.env.SEI_RPC ?? "https://evm-rpc.sei-apis.com", + url: process.env.SEI_RPC ?? 'https://evm-rpc.sei-apis.com', chainId: 1329, accounts, }, // testnets arbitrumSepolia: { - url: process.env.ARBITRUM_SEPOLIA_RPC ?? "https://arbitrum-sepolia-rpc.publicnode.com", + url: + process.env.ARBITRUM_SEPOLIA_RPC ?? + 'https://arbitrum-sepolia-rpc.publicnode.com', chainId: 421614, accounts, }, optimismSepolia: { - url: process.env.OPTIMISM_SEPOLIA_RPC ?? "https://sepolia.optimism.io", + url: process.env.OPTIMISM_SEPOLIA_RPC ?? 'https://sepolia.optimism.io', chainId: 11155420, accounts, }, @@ -139,148 +144,214 @@ const config: HardhatUserConfig = { etherscan: { enabled: true, apiKey: { - mainnet: process.env.MAINNET_ETHERSCAN_KEY ?? "", - ethereum: process.env.MAINNET_ETHERSCAN_KEY ?? "", - polygon: process.env.POLYGON_ETHERSCAN_KEY ?? "", - arbitrumOne: process.env.ARBITRUM_ETHERSCAN_KEY ?? "", - optimism: process.env.OPTIMISM_ETHERSCAN_KEY ?? "", - base: process.env.BASE_ETHERSCAN_KEY ?? "", - bsc: process.env.BSC_ETHERSCAN_KEY ?? "", - avalanche: process.env.AVALANCHE_ETHERSCAN_KEY ?? "", - linea: process.env.LINEA_ETHERSCAN_KEY ?? "", - scroll: process.env.SCROLL_ETHERSCAN_KEY ?? "", - blast: process.env.BLAST_ETHERSCAN_KEY ?? "", - mantle: process.env.MANTLE_ETHERSCAN_KEY ?? "", - gnosis: process.env.GNOSIS_ETHERSCAN_KEY ?? "", - sonic: process.env.SONIC_ETHERSCAN_KEY ?? "", - unichain: process.env.UNICHAIN_ETHERSCAN_KEY ?? "", - berachain: process.env.BERACHAIN_ETHERSCAN_KEY ?? "", - sei: process.env.SEI_ETHERSCAN_KEY ?? "", - arbitrumSepolia: process.env.ARBITRUM_ETHERSCAN_KEY ?? "", - optimismSepolia: process.env.OPTIMISM_ETHERSCAN_KEY ?? "", + mainnet: process.env.MAINNET_ETHERSCAN_KEY ?? '', + ethereum: process.env.MAINNET_ETHERSCAN_KEY ?? '', + polygon: process.env.POLYGON_ETHERSCAN_KEY ?? '', + arbitrumOne: process.env.ARBITRUM_ETHERSCAN_KEY ?? '', + optimism: process.env.OPTIMISM_ETHERSCAN_KEY ?? '', + base: process.env.BASE_ETHERSCAN_KEY ?? '', + bsc: process.env.BSC_ETHERSCAN_KEY ?? '', + avalanche: process.env.AVALANCHE_ETHERSCAN_KEY ?? '', + linea: process.env.LINEA_ETHERSCAN_KEY ?? '', + scroll: process.env.SCROLL_ETHERSCAN_KEY ?? '', + blast: process.env.BLAST_ETHERSCAN_KEY ?? '', + mantle: process.env.MANTLE_ETHERSCAN_KEY ?? '', + gnosis: process.env.GNOSIS_ETHERSCAN_KEY ?? '', + sonic: process.env.SONIC_ETHERSCAN_KEY ?? '', + unichain: process.env.UNICHAIN_ETHERSCAN_KEY ?? '', + berachain: process.env.BERACHAIN_ETHERSCAN_KEY ?? '', + sei: process.env.SEI_ETHERSCAN_KEY ?? '', + arbitrumSepolia: process.env.ARBITRUM_ETHERSCAN_KEY ?? '', + optimismSepolia: process.env.OPTIMISM_ETHERSCAN_KEY ?? '', }, customChains: [ { - network: "ethereum", + network: 'ethereum', chainId: 1, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=1", browserURL: "https://etherscan.io" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=1', + browserURL: 'https://etherscan.io', + }, }, { - network: "optimism", + network: 'optimism', chainId: 10, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=10", browserURL: "https://optimistic.etherscan.io" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=10', + browserURL: 'https://optimistic.etherscan.io', + }, }, { - network: "bsc", + network: 'bsc', chainId: 56, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=56", browserURL: "https://bscscan.com" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=56', + browserURL: 'https://bscscan.com', + }, }, { - network: "polygon", + network: 'polygon', chainId: 137, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=137", browserURL: "https://polygonscan.com" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=137', + browserURL: 'https://polygonscan.com', + }, }, { - network: "mantle", + network: 'mantle', chainId: 5000, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=5000", browserURL: "https://mantlescan.xyz" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=5000', + browserURL: 'https://mantlescan.xyz', + }, }, { - network: "arbitrumOne", + network: 'arbitrumOne', chainId: 42161, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=42161", browserURL: "https://arbiscan.io" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=42161', + browserURL: 'https://arbiscan.io', + }, }, { - network: "avalanche", + network: 'avalanche', chainId: 43114, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=43114", browserURL: "https://snowscan.xyz" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=43114', + browserURL: 'https://snowscan.xyz', + }, }, { - network: "linea", + network: 'linea', chainId: 59144, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=59144", browserURL: "https://lineascan.build" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=59144', + browserURL: 'https://lineascan.build', + }, }, { - network: "base", + network: 'base', chainId: 8453, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=8453", browserURL: "https://basescan.org" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=8453', + browserURL: 'https://basescan.org', + }, }, { - network: "gnosis", + network: 'gnosis', chainId: 100, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=100", browserURL: "https://gnosisscan.io" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=100', + browserURL: 'https://gnosisscan.io', + }, }, { - network: "blast", + network: 'blast', chainId: 81457, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=81457", browserURL: "https://blastscan.io" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=81457', + browserURL: 'https://blastscan.io', + }, }, { - network: "scroll", + network: 'scroll', chainId: 534352, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=534352", browserURL: "https://scrollscan.com" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=534352', + browserURL: 'https://scrollscan.com', + }, }, { - network: "mode", + network: 'mode', chainId: 34443, - urls: { apiURL: "https://explorer.mode.network/api", browserURL: "https://explorer.mode.network" }, + urls: { + apiURL: 'https://explorer.mode.network/api', + browserURL: 'https://explorer.mode.network', + }, }, { - network: "sonic", + network: 'sonic', chainId: 146, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=146", browserURL: "https://sonicscan.org" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=146', + browserURL: 'https://sonicscan.org', + }, }, { - network: "unichain", + network: 'unichain', chainId: 130, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=130", browserURL: "https://uniscan.xyz" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=130', + browserURL: 'https://uniscan.xyz', + }, }, { - network: "berachain", + network: 'berachain', chainId: 80094, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=80094", browserURL: "https://berascan.com" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=80094', + browserURL: 'https://berascan.com', + }, }, { - network: "ink", + network: 'ink', chainId: 57073, - urls: { apiURL: "https://explorer.inkonchain.com/api", browserURL: "https://explorer.inkonchain.com" }, + urls: { + apiURL: 'https://explorer.inkonchain.com/api', + browserURL: 'https://explorer.inkonchain.com', + }, }, { - network: "soneium", + network: 'soneium', chainId: 1868, - urls: { apiURL: "https://soneium.blockscout.com/api", browserURL: "https://soneium.blockscout.com" }, + urls: { + apiURL: 'https://soneium.blockscout.com/api', + browserURL: 'https://soneium.blockscout.com', + }, }, { - network: "worldchain", + network: 'worldchain', chainId: 480, - urls: { apiURL: "https://api.etherscan.io/v2/api?chainid=480", browserURL: "https://worldscan.org" }, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=480', + browserURL: 'https://worldscan.org', + }, }, { - network: "sei", + network: 'sei', chainId: 1329, - urls: { apiURL: "https://seitrace.com/pacific-1/api", browserURL: "https://seitrace.com" }, + urls: { + apiURL: 'https://seitrace.com/pacific-1/api', + browserURL: 'https://seitrace.com', + }, }, { - network: "arbitrumSepolia", + network: 'arbitrumSepolia', chainId: 421614, - urls: { apiURL: "https://api-sepolia.arbiscan.io/api", browserURL: "https://sepolia.arbiscan.io" }, + urls: { + apiURL: 'https://api-sepolia.arbiscan.io/api', + browserURL: 'https://sepolia.arbiscan.io', + }, }, { - network: "optimismSepolia", + network: 'optimismSepolia', chainId: 11155420, - urls: { apiURL: "https://api-sepolia-optimistic.etherscan.io/api", browserURL: "https://sepolia-optimism.etherscan.io" }, + urls: { + apiURL: 'https://api-sepolia-optimistic.etherscan.io/api', + browserURL: 'https://sepolia-optimism.etherscan.io', + }, }, ], }, typechain: { - outDir: "typechain", + outDir: 'typechain', alwaysGenerateOverloads: true, }, paths: { - sources: "./src", - scripts: "./scripts", - cache: "./cache-hh", - artifacts: "./artifacts", + sources: './src', + scripts: './scripts', + cache: './cache-hh', + artifacts: './artifacts', }, }; diff --git a/package.json b/package.json index 5a28b2d..feeaa09 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "hardhat": "^2.22.7", "ts-node": "^10.9.0", "typescript": "^5.0.0" + }, + "dependencies": { + "@layerzerolabs/lz-v2-utilities": "^3.0.168" } } diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index f609d5d..1eef43e 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -1,159 +1,197 @@ /** - * Script 1 — Bridge AAVE (Arbitrum) → PEPE (Base) via Relay.link + * Script 1 — Bridge AAVE (Polygon PoS) → AAVE (Base) via Relay.link * * Flow: - * 1. Fetch a Relay.link /quote/v2 for AAVE→PEPE cross-chain swap. - * The quote is requested for (inputAmount - feeAmount) to account for the - * pre-bridge fee we take first. - * 2. Parse the approve step to extract the Relay spender address. - * 3. Build either a monolithic or modular execution payload (controlled by - * USE_MODULAR env var). - * 4. Call AllowanceHolder.exec → router.performExecution / performModularExecution. - * - * The script spends the signer’s full on-chain balance of AAVE on Arbitrum as the input amount (fund the wallet and approve AH as needed). + * 1. Spend half of the signer’s Polygon AAVE balance twice: first via + * performExecution (monolithic), then via performModularExecution, each leg + * using balance/2 of the initial snapshot. + * 2. For each leg: Relay.link /quote/v2 for AAVE→AAVE cross-chain swap for the + * net relay amount (slice − feeAmount). + * 3. Parse approve + deposit steps → build mono or modular payload. + * 4. AllowanceHolder.exec → router.performExecution / performModularExecution. * * Usage: - * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts - * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts + * Polygon USDC (Circle) → Base USDC: + * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts usdc-polygon-base * - * Notes on the Relay.link quote API: - * - steps[0] is an ERC-20 approve (or absent for native input). - * The approve spender can be decoded from steps[0].items[0].data.data bytes 16..36. - * - steps[1] is the actual deposit call: steps[1].items[0].data.{ to, data }. - * - Relay quotes EXACT_INPUT so the amount in the deposit calldata is already - * correct for the quoted amount; we do NOT splice it at runtime. + * Router addresses: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain` in config.ts. + * Override Polygon with `ROUTER_CHAIN_137` or legacy `ROUTER_ADDRESS` if needed. */ -import axios from 'axios'; import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; dotenv.config(); import { CHAIN_IDS, - ROUTER_ADDRESS, + routerAddressForChain, TOKENS, FEE_BPS, bpsOf, RPC, - RELAY_API_KEY, ALLOWANCE_HOLDER, } from './config'; -import { execViaAH } from './utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, getWalletErc20Balance } from './utils/erc20'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; +import { + encodeApprove, + encodeTransfer, + getWalletErc20Balance, +} from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - MonolithicExecution, - NO_FEE, - NO_SWAP, - ZERO_ADDRESS, -} from './utils/contractTypes'; - -// ─── Relay.link quote ───────────────────────────────────────────────────────── - -interface RelayStep { - items: Array<{ - data: { - to?: string; - data?: string; - }; - }>; -} +import { MonolithicExecution, NO_FEE, NO_SWAP } from './utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; +import { sleep } from './utils/sleep'; +import { logTxnSummary } from './utils/txnLogSummary'; -interface RelayQuoteResponse { - steps: RelayStep[]; +/** Router on Polygon — quotes + modular recipient target must match executing chain deployment. */ +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildMonolithicExecution( + signerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + relaySpender: string, + depositTarget: string, + depositData: string, +): MonolithicExecution { + return { + input: { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount, + }, + preFee: { + receiver: signerAddress, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: depositTarget, + approvalSpender: relaySpender, + value: 0n, + data: depositData, + amountPositions: [], + useFinalAmountAsValue: false, + }, + }; } -/** - * Fetches a cross-chain quote from Relay.link for AAVE→PEPE. - * - * @param routerAddress The router contract (= the "user" that sends the deposit) - * @param recipient The final recipient on Base (signer's EOA) - * @param amount Net amount after fee, in AAVE wei - */ -async function fetchRelayQuote( +function buildModularActions( + signerAddress: string, routerAddress: string, - recipient: string, - amount: bigint, -): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - }; - if (RELAY_API_KEY) { - headers['x-api-key'] = RELAY_API_KEY; - } + inputAmount: bigint, + feeAmount: bigint, + bridgeAmount: bigint, + relaySpender: string, + depositTarget: string, + depositData: string, +): ModularAction[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.AAVE_POLYGON, + signerAddress, + routerAddress, + inputAmount, + ]); - const body = { - user: routerAddress, - recipient, - originChainId: CHAIN_IDS.ARBITRUM, - destinationChainId: CHAIN_IDS.BASE, - originCurrency: TOKENS.AAVE_ARB, - destinationCurrency: TOKENS.AAVE_BASE, - tradeType: 'EXACT_INPUT', - amount: amount.toString(), - }; + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.AAVE_POLYGON, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.AAVE_POLYGON, encodeApprove(relaySpender, bridgeAmount)); + exec.call(depositTarget, depositData); + return exec.toActions(); +} + +async function executeLeg(args: { + label: string; + useModular: boolean; + signer: ethers.Wallet; + signerAddress: string; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { label, useModular, signer, signerAddress, inputAmount, routerIface } = + args; + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; - const response = await axios.post( - 'https://api.relay.link/quote/v2', - body, - { headers }, + console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + console.log(`Input slice: ${ethers.formatUnits(inputAmount, 18)} AAVE`); + console.log( + `Fee (pre-bridge): ${ethers.formatUnits(feeAmount, 18)} (${FEE_BPS} bps)`, ); - return response.data; -} + console.log(`Relay amount: ${ethers.formatUnits(bridgeAmount, 18)}`); -/** - * Parses the Relay.link quote to extract: - * - relaySpender: the address that needs ERC-20 approval (from approve step calldata) - * - depositTarget: the contract to call with depositData - * - depositData: the calldata for the deposit call - */ -function parseRelayQuote(quote: RelayQuoteResponse): { - relaySpender: string; - depositTarget: string; - depositData: string; -} { - const approveIface = new ethers.Interface([ - 'function approve(address spender, uint256 amount) external returns (bool)', - ]); + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); - const approveStep = quote.steps[0]; - const approveDataHex = approveStep.items[0].data.data ?? ''; - let relaySpender: string; - try { - relaySpender = ethers.getAddress( - approveIface.decodeFunctionData('approve', approveDataHex)[0], + let execCalldata: string; + if (useModular) { + const actions = buildModularActions( + signerAddress, + ROUTER_POLYGON, + inputAmount, + feeAmount, + bridgeAmount, + relaySpender, + depositTarget, + depositData, + ); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + actions, + ]); + } else { + const execPayload = buildMonolithicExecution( + signerAddress, + inputAmount, + feeAmount, + relaySpender, + depositTarget, + depositData, ); - } catch { - /** Some routes use Permit2 signatures instead of naked approve; spender may still appear in abi.encode-like layout. Fallback: last 20 bytes of first argument word. */ - const normalized = approveDataHex.startsWith('0x') ? approveDataHex.slice(2) : approveDataHex; - if (normalized.length < 8 + 64) { - throw new Error('Relay approve step calldata too short for fallback spender parse'); - } - const spender40 = normalized.slice(8 + 24, 8 + 24 + 40); - - relaySpender = ethers.getAddress('0x' + spender40); + execCalldata = routerIface.encodeFunctionData('performExecution', [ + execPayload, + ]); } - const depositStep = quote.steps[1]; - const depositItem = depositStep.items[0].data; - const depositTarget = depositItem.to ?? ''; - const depositData = depositItem.data ?? '0x'; + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); - return { relaySpender, depositTarget, depositData }; -} + console.log('Sending AllowanceHolder.exec...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + execCalldata, + ); -// ─── Monolithic builder ─────────────────────────────────────────────────────── + const modeLabel = useModular ? 'Modular' : 'Monolithic'; + logTxnSummary( + `Polygon AAVE → Base AAVE — Relay — ${modeLabel}`, + CHAIN_IDS.POLYGON, + receipt, + ); +} -/** - * Builds a MonolithicExecution that: - * - Pulls inputAmount of AAVE from user via AH - * - Sends feeAmount of AAVE to signer as pre-bridge fee - * - Approves Relay spender for (inputAmount - feeAmount) - * - Calls Relay deposit target with deposit calldata (amount already correct in calldata) - */ -function buildMonolithicExecution( +function buildMonolithicExecutionUsdcPolygonToBase( signerAddress: string, inputAmount: bigint, feeAmount: bigint, @@ -164,7 +202,7 @@ function buildMonolithicExecution( return { input: { user: signerAddress, - inputToken: TOKENS.AAVE_ARB, + inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount, }, preFee: { @@ -178,27 +216,13 @@ function buildMonolithicExecution( approvalSpender: relaySpender, value: 0n, data: depositData, - amountPositions: [], // Relay calldata is already for the correct amount + amountPositions: [], useFinalAmountAsValue: false, }, }; } -// ─── Modular builder ────────────────────────────────────────────────────────── - -/** - * Builds an Action array that achieves the same flow as monolithic but - * as discrete steps: - * [0] Pull AAVE from user via AH.transferFrom (AH already has allowance) - * [1] Transfer feeAmount AAVE to signer (pre-bridge fee) - * [2] Approve Relay spender for bridgeAmount AAVE - * [3] Call Relay deposit - * - * Note: In the modular path the router calls AH.transferFrom directly as an - * action, since AH has the transient allowance granted by AH.exec() on entry. - * AH.transferFrom selector: 0x15dacbea (address token, address owner, address recipient, uint256 amount) - */ -function buildModularActions( +function buildModularActionsUsdcPolygonToBase( signerAddress: string, routerAddress: string, inputAmount: bigint, @@ -208,12 +232,11 @@ function buildModularActions( depositTarget: string, depositData: string, ): ModularAction[] { - // AH.transferFrom(token, owner, recipient, amount) = 0x15dacbea const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.AAVE_ARB, + TOKENS.USDC_POLYGON_CIRCLE, signerAddress, routerAddress, inputAmount, @@ -221,70 +244,52 @@ function buildModularActions( const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.AAVE_ARB, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.AAVE_ARB, encodeApprove(relaySpender, bridgeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(relaySpender, bridgeAmount)); exec.call(depositTarget, depositData); return exec.toActions(); } -// ─── Main ───────────────────────────────────────────────────────────────────── - -async function main() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } +async function executeLegUsdcPolygonToBase(args: { + label: string; + useModular: boolean; + signer: ethers.Wallet; + signerAddress: string; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { label, useModular, signer, signerAddress, inputAmount, routerIface } = + args; - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.AAVE_ARB; - const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (inputAmount === 0n) { - throw new Error( - `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with AAVE on Arbitrum first.`, - ); - } const feeAmount = bpsOf(inputAmount, FEE_BPS); const bridgeAmount = inputAmount - feeAmount; - const useModular = false; - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ADDRESS}`); - console.log(`Input token: ${inputToken}`); - console.log(`Input amount: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`); + console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + console.log(`Input slice: ${ethers.formatUnits(inputAmount, 6)} USDC`); console.log( - `Fee amount: ${ethers.formatUnits(feeAmount, inputDecimals)} (${FEE_BPS} bps)`, + `Fee (pre-bridge): ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`, ); - console.log(`Bridge amount: ${ethers.formatUnits(bridgeAmount, inputDecimals)}`); - console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); - console.log(''); + console.log(`Relay amount: ${ethers.formatUnits(bridgeAmount, 6)}`); - // Fetch Relay.link quote for bridgeAmount console.log('Fetching Relay.link quote...'); - const quote = await fetchRelayQuote( - ROUTER_ADDRESS, - signerAddress, - bridgeAmount, - ); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); console.log(`Relay spender: ${relaySpender}`); console.log(`Deposit target: ${depositTarget}`); - console.log(''); - const routerIface = new ethers.Interface(ROUTER_ABI); let execCalldata: string; - if (useModular) { - const actions = buildModularActions( + const actions = buildModularActionsUsdcPolygonToBase( signerAddress, - ROUTER_ADDRESS, + ROUTER_POLYGON, inputAmount, feeAmount, bridgeAmount, @@ -295,9 +300,8 @@ async function main() { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ actions, ]); - console.log('Using performModularExecution'); } else { - const exec = buildMonolithicExecution( + const execPayload = buildMonolithicExecutionUsdcPolygonToBase( signerAddress, inputAmount, feeAmount, @@ -305,21 +309,169 @@ async function main() { depositTarget, depositData, ); - execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); - console.log('Using performExecution (monolithic)'); + execCalldata = routerIface.encodeFunctionData('performExecution', [ + execPayload, + ]); } - console.log('Sending AllowanceHolder.exec transaction...'); + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + ); + + console.log('Sending AllowanceHolder.exec...'); const receipt = await execViaAH( signer, - ROUTER_ADDRESS, // operator - TOKENS.AAVE_ARB, // token to grant allowance for - inputAmount, // amount - ROUTER_ADDRESS, // target (the router) + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + ROUTER_POLYGON, execCalldata, ); - console.log(`\nSuccess! Gas used: ${receipt.gasUsed.toString()}`); + const modeLabel = useModular ? 'Modular' : 'Monolithic'; + logTxnSummary( + `Polygon USDC → Base USDC — Relay — ${modeLabel}`, + CHAIN_IDS.POLYGON, + receipt, + ); +} + +async function mainUsdcPolygonToBaseRelay() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with Polygon native Circle USDC first.`, + ); + } + + const legAmount = walletBalance / 2n; + if (legAmount === 0n) { + throw new Error( + `Wallet balance (${walletBalance}) is too small to split into two nonzero halves.`, + ); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Input token: ${inputToken}`); + console.log(`Full balance: ${ethers.formatUnits(walletBalance, 6)} USDC`); + console.log( + `Per execution: ${ethers.formatUnits(legAmount, 6)} USDC (50% snapshots)`, + ); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + await executeLegUsdcPolygonToBase({ + label: '1/2 Monolithic', + useModular: false, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log('Sleeping ~3s before modular execution...'); + await sleep(3000); + + await executeLegUsdcPolygonToBase({ + label: '2/2 Modular', + useModular: true, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log( + '\nCompleted monolithic then modular executions (Relay Polygon USDC → Base USDC).', + ); +} + +async function main() { + const relayE2eCase = process.argv[2]?.toLowerCase(); + if (relayE2eCase === 'usdc-polygon-base' || relayE2eCase === 'usdc') { + await mainUsdcPolygonToBaseRelay(); + return; + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with Polygon AAVE first.`, + ); + } + + const legAmount = walletBalance / 2n; + if (legAmount === 0n) { + throw new Error( + `Wallet balance (${walletBalance}) is too small to split into two nonzero halves.`, + ); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Input token: ${inputToken}`); + console.log(`Full balance: ${ethers.formatUnits(walletBalance, 18)} AAVE`); + console.log( + `Per execution: ${ethers.formatUnits(legAmount, 18)} AAVE (50% snapshots)`, + ); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + await executeLeg({ + label: '1/2 Monolithic', + useModular: false, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log('Sleeping ~3s before modular execution...'); + await sleep(3000); + + await executeLeg({ + label: '2/2 Modular', + useModular: true, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log( + '\nCompleted monolithic then modular executions (Relay Polygon → Base AAVE).', + ); } main().catch((err) => { diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index d77d3d9..cb3ace2 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -11,15 +11,58 @@ export const CHAIN_IDS = { ETHEREUM: 1, ARBITRUM: 42161, BASE: 8453, + /** Polygon PoS mainnet — used by e2e scripts as the source chain. */ + POLYGON: 137, } as const; +/** Base URL for explorer transaction pages: `${prefix}${txHash}`. */ +export const BLOCK_EXPLORER_TX_PREFIX: Record = { + [CHAIN_IDS.ETHEREUM]: 'https://etherscan.io/tx/', + [CHAIN_IDS.ARBITRUM]: 'https://arbiscan.io/tx/', + [CHAIN_IDS.BASE]: 'https://basescan.org/tx/', + [CHAIN_IDS.POLYGON]: 'https://polygonscan.com/tx/', +}; + // ─── Contract addresses ─────────────────────────────────────────────────────── /** 0x AllowanceHolder — same address on every EVM chain */ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; -/** Deployed combined unchecked router instance (set via env after deployment) */ -export const ROUTER_ADDRESS: string = '0x33cBEF62f74f5204651D4C5Dcc3fd8E56A01F2aF'; +/** + * Deployed `BungeeOpenRouterV2Unchecked` — one address per chain used by e2e scripts. + * Override per chain with env `ROUTER_CHAIN_` (e.g. ROUTER_CHAIN_1 for Ethereum). + * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. + */ +export const ROUTER_BY_CHAIN_ID: Record = { + [CHAIN_IDS.POLYGON]: '0x23D5aFEF7cE44257366D9ef6de80Ea334FAa9d25', + [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', + [CHAIN_IDS.BASE]: '0x96E8c261fCCDFca2CCffe8b4A33dC8a65f153785', +}; + +const ADDR_HEX_RE = /^0x[a-fA-F0-9]{40}$/; + +/** + * Resolve router contract address for execution quotes / AH.exec on `chainId`. + * + * Priority: `ROUTER_CHAIN_` env → {@link ROUTER_BY_CHAIN_ID} → `ROUTER_ADDRESS` env. + */ +export function routerAddressForChain(chainId: number): string { + const envSpecific = process.env[`ROUTER_CHAIN_${chainId}`]?.trim(); + if (envSpecific && ADDR_HEX_RE.test(envSpecific)) { + return envSpecific; + } + const mapped = ROUTER_BY_CHAIN_ID[chainId]; + if (mapped) { + return mapped; + } + const legacy = process.env.ROUTER_ADDRESS?.trim(); + if (legacy && ADDR_HEX_RE.test(legacy)) { + return legacy; + } + throw new Error( + `routerAddressForChain(${chainId}): no ROUTER_BY_CHAIN_ID entry and neither ROUTER_CHAIN_${chainId} nor ROUTER_ADDRESS is set.`, + ); +} /** Standard ERC-20 "native" sentinel used by CurrencyLib */ export const NATIVE_TOKEN_ADDRESS = @@ -30,12 +73,54 @@ export const NATIVE_TOKEN_ADDRESS = export const TOKENS = { AAVE_ARB: '0xba5DdD1f9d7F570dc94a51479a000E3BCE967196', AAVE_ETH: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', + /** Polygon PoS AAVE (aPolAAVE ERC-4626-backed). */ + AAVE_POLYGON: '0xD6DF932A45C0f255f85145f286eA0b292B21C90B', USDC_ARB: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', USDC_BASE: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', USDC_ETH: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + /** Bridged PoS-USDC on Polygon (6 decimals); not burnable via Circle CCTP. */ + USDC_POLYGON: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + /** Circle native USDC on Polygon — required for CCTP `depositForBurn` on PoS. */ + USDC_POLYGON_CIRCLE: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + /** Canonical wrapped gas token on Polygon PoS (18 decimals). */ + WMATIC_POLYGON: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', AAVE_BASE: '0x63706e401c06ac8513145b7687a14804d17f814b', + /** + * USDT0 on Polygon PoS — the inner ERC-20 token that the OFT Adapter wraps (6 decimals). + * Users approve the OFT Adapter to pull this token, then call Adapter.send(). + */ + USDT0_POLYGON: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + /** + * USDT0 on Base — a native OFT (the token contract itself IS the OFT; no separate adapter). + * Bridged in via LayerZero from Polygon via the USDT0 OFT Adapter on Polygon. + */ + USDT0_BASE: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', + /** + * Arbitrum USDT — inner token for the USDT0 OFT stack (6 decimals). + * OFT adapter pulls this ERC-20, then calls LayerZero `send()` to Base USDT0. + * Matches bungee `oftSupportedTokens` / {@link USDT0_OFT_ADAPTER_ARBITRUM}. + */ + USDT0_ARB: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', } as const; +/** + * USDT0 OFT Adapter on Polygon PoS (legacy / alternate route; Polygon→Base may hit LZ NoPeer). + * Type: OFT_ADAPTER — requires ERC-20 approval of TOKENS.USDT0_POLYGON before calling send(). + */ +export const USDT0_OFT_ADAPTER_POLYGON = '0x6ba10300f0dc58b7a1e4c0e41f5dabb7d7829e13'; + +/** + * USDT0 OFT Adapter on Arbitrum — quote `send()` here; approve adapter to spend {@link TOKENS.USDT0_ARB}. + * Seed doc: `oftId` `42161-0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9`, `adapterAddress` on Arbitrum. + */ +export const USDT0_OFT_ADAPTER_ARBITRUM = + '0x14e4a1b13Bf7F943c8fF7C51fb60fa964A298d92'; + +/** + * LayerZero v2 endpoint ID for Polygon PoS (EID 30109). + */ +export const POLYGON_LZ_EID = 30109; + // ─── CCTP v2 configuration ──────────────────────────────────────────────────── export interface CctpChainConfig { @@ -46,6 +131,11 @@ export interface CctpChainConfig { } export const CCTP_CONFIG: Record = { + [CHAIN_IDS.POLYGON]: { + tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d', + cctpDomain: 7, + usdcAddress: TOKENS.USDC_POLYGON_CIRCLE, + }, [CHAIN_IDS.ARBITRUM]: { tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d', cctpDomain: 3, @@ -68,7 +158,7 @@ export const CCTP_CONFIG: Record = { /** Arbitrum Delayed Inbox — accepts ETH deposits via depositEth() */ export const ARBITRUM_INBOX = '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'; -// ─── Stargate Native Pool (ETH Arbitrum → ETH Base) ───────────────────────── +// ─── Stargate pools ─────────────────────────────────────────────────────────── /** * Stargate Native ETH OFT adapter on Arbitrum. @@ -77,9 +167,25 @@ export const ARBITRUM_INBOX = '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'; */ export const STARGATE_NATIVE_ARB = '0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F'; -/** LayerZero v2 endpoint ID for Base (EID 30184). Used in Stargate sendParam.dstEid. */ +/** + * Stargate v2 native ETH OFT pool on Base. + * send() requires msg.value = amountLD + nativeFee; bridges ETH to Arbitrum/other chains. + */ +export const STARGATE_NATIVE_BASE = '0xdc181Bd607330aeeBEF6ea62e03e5e1Fb4B6F7C7'; + +/** + * Stargate v2 USDC pool on Polygon PoS. + * NOTE: Polygon has no StargatePoolNative for POL/MATIC — only USDC and USDT + * pools are deployed. Bridge token is Circle USDC, not the chain native token. + */ +export const STARGATE_USDC_POLYGON = '0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4'; + +/** LayerZero v2 endpoint ID for Base (EID 30184). */ export const BASE_LZ_EID = 30184; +/** LayerZero v2 endpoint ID for Arbitrum (EID 30110). */ +export const ARBITRUM_LZ_EID = 30110; + /** * Byte offset of sendParam.amountLD within the Stargate send() calldata (after the 4-byte selector). * Layout: selector(4) + head[sendParam_ptr(32) + nativeFee(32) + lzTokenFee(32) + refundAddr(32)] + @@ -100,6 +206,8 @@ export function bpsOf(amount: bigint, bps: number): bigint { export const RPC = { ARBITRUM: process.env.ARBITRUM_RPC ?? 'https://arb1.arbitrum.io/rpc', + POLYGON: + process.env.POLYGON_RPC ?? 'https://polygon-bor.publicnode.com', ETHEREUM: process.env.ETHEREUM_RPC ?? 'https://eth.llamarpc.com', BASE: process.env.BASE_RPC ?? 'https://mainnet.base.org', } as const; diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index 101b13d..b22647d 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -19,8 +19,11 @@ * Uses the signer’s full AAVE balance on Ethereum mainnet as swap input. * * Usage: - * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts - * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts + * USE_MODULAR=true PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts + * + * Router on Ethereum mainnet: set `ROUTER_CHAIN_1` or legacy `ROUTER_ADDRESS` + * ({@link ROUTER_BY_CHAIN_ID} has no Ethereum entry). * * Notes: * - The router must retain enough ETH after the swap to cover both the fee @@ -37,7 +40,7 @@ dotenv.config(); import { CHAIN_IDS, - ROUTER_ADDRESS, + routerAddressForChain, TOKENS, ARBITRUM_INBOX, FEE_BPS, @@ -58,6 +61,8 @@ import { ZERO_ADDRESS, } from './utils/contractTypes'; +const ROUTER_ETHEREUM = routerAddressForChain(CHAIN_IDS.ETHEREUM); + // ─── Arbitrum retryable fee estimation ─────────────────────────────────────── /** @@ -284,7 +289,7 @@ async function main() { const useModular = true; console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ADDRESS}`); + console.log(`Router: ${ROUTER_ETHEREUM}`); console.log(`Input token: ${inputToken}`); console.log( `Input: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`, @@ -295,7 +300,7 @@ async function main() { // Fetch OpenOcean quote (AAVE → ETH on Ethereum) console.log('Fetching OpenOcean swap quote (AAVE→ETH Ethereum)...'); const { ooRouterAddress, swapData, minAmountOut, estimatedOut } = - await fetchOpenOceanSwapQuote(ROUTER_ADDRESS, inputAmount); + await fetchOpenOceanSwapQuote(ROUTER_ETHEREUM, inputAmount); const feeAmount = bpsOf(estimatedOut, FEE_BPS); console.log(`OO Router: ${ooRouterAddress}`); @@ -326,7 +331,7 @@ async function main() { if (useModular) { const actions = buildModularActions( signerAddress, - ROUTER_ADDRESS, + ROUTER_ETHEREUM, inputAmount, feeAmount, minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n, @@ -356,10 +361,10 @@ async function main() { console.log('Sending AllowanceHolder.exec transaction...'); const receipt = await execViaAH( signer, - ROUTER_ADDRESS, + ROUTER_ETHEREUM, TOKENS.AAVE_ETH, inputAmount, - ROUTER_ADDRESS, + ROUTER_ETHEREUM, execCalldata, 0n, // no ETH needed from caller; ETH comes from the swap output ); diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index ec3efbc..3ff723e 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -1,27 +1,17 @@ /** - * Script 2 — Swap AAVE→USDC on Arbitrum, then bridge USDC to Base via CCTP v2 + * Script 2 — Swap AAVE→USDC on Polygon, then bridge USDC to Base via CCTP v2 * - * Flow: - * 1. Fetch an OpenOcean swap quote for AAVE→USDC on Arbitrum. - * 2. Build CCTP v2 depositForBurn calldata with a zero amount placeholder - * at byte offset 4 (the first parameter). - * 3. Build either a monolithic or modular execution payload. - * - Monolithic: swap inside the router using the decoded swap return amount, - * take a post-swap fee in USDC, splice finalAmount into depositForBurn, - * approve TOKEN_MESSENGER, call TOKEN_MESSENGER. - * - Modular: discrete actions — pull → approve(oo) → swap(oo) → transfer fee → - * approve(cctp) → staticcall balanceOf → call depositForBurn (splice balance→amount). - * 4. Call AllowanceHolder.exec → router.performExecution / performModularExecution. + * OpenOcean must output Circle’s **native** Polygon USDC (`USDC_POLYGON_CIRCLE`). + * Bridged USDC (`0x2791…`, USDC.e) is rejected by TokenMessenger (“Burn token not supported”). * - * Uses the signer’s full AAVE balance on Arbitrum as swap input (fund the wallet and approve AH as needed). + * Each run uses half of the initial AAVE snapshot: monolithic then modular. * * Usage: - * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts - * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts + * Polygon native USDC → Base USDC via CCTP only (no OpenOcean swap): + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts usdc-polygon-base * - * CCTP v2 fast path: - * minFinalityThreshold=1000 (1000 confirmations, ~instant finality on supported chains) - * maxFee set to a small value; pass 0 for the standard (slower) path. + * Router on Polygon: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(137)`. */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -30,7 +20,7 @@ dotenv.config(); import { CHAIN_IDS, - ROUTER_ADDRESS, + routerAddressForChain, TOKENS, CCTP_CONFIG, FEE_BPS, @@ -47,10 +37,12 @@ import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, NO_FEE, - ZERO_ADDRESS, + NO_SWAP, } from './utils/contractTypes'; +import { sleep } from './utils/sleep'; +import { logTxnSummary } from './utils/txnLogSummary'; -// ─── OpenOcean swap quote ───────────────────────────────────────────────────── +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); interface OpenOceanSwapQuoteResponse { data: { @@ -63,15 +55,6 @@ interface OpenOceanSwapQuoteResponse { }; } -/** - * Fetches a swap quote from OpenOcean for AAVE→USDC on Arbitrum. - * The router address is used as both sender and account so OpenOcean - * routes the swap through the router itself. - * - * @param routerAddress Address that will execute the swap (needs approval) - * @param inputAmount Amount of AAVE in wei - * @param slippageBps Slippage tolerance in basis points (e.g. 100 = 1%) - */ async function fetchOpenOceanSwapQuote( routerAddress: string, inputAmount: bigint, @@ -83,19 +66,19 @@ async function fetchOpenOceanSwapQuote( estimatedOut: bigint; }> { const params: Record = { - inTokenAddress: TOKENS.AAVE_ARB, - outTokenAddress: TOKENS.USDC_ARB, - amount: ethers.formatUnits(inputAmount, 18), // OO expects human-readable amount + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), slippage: (slippageBps / 100).toString(), sender: routerAddress, account: routerAddress, - gasPrice: '1', // gwei; doesn't affect routing + gasPrice: '1', }; if (OPEN_OCEAN_API_KEY) { - params['apikey'] = OPEN_OCEAN_API_KEY; + params.apikey = OPEN_OCEAN_API_KEY; } - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; const response = await axios.get(url, { params }); const q = response.data.data; @@ -107,18 +90,6 @@ async function fetchOpenOceanSwapQuote( }; } -// ─── CCTP depositForBurn calldata ───────────────────────────────────────────── - -/** - * Builds CCTP v2 depositForBurn calldata. - * `amount` is set to 0 as a placeholder; it will be spliced in at runtime - * (offset 4 in the calldata, i.e. amountPositions=[4] in MonolithicExecution). - * - * For the modular path a STATICCALL balanceOf + splice is used instead. - * - * Fast path: minFinalityThreshold=1000, maxFee=small value - * Standard path: minFinalityThreshold=2000, maxFee=0 - */ function buildDepositForBurnCalldata( recipientAddress: string, burnToken: string, @@ -129,35 +100,21 @@ function buildDepositForBurnCalldata( 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', ]); - // Pad the recipient address to bytes32 const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); - - // maxFee: small fee for fast path (e.g. 1 USDC = 1_000_000 units), 0 for standard const maxFee = fastPath ? 1_000_000n : 0n; const minFinalityThreshold = fastPath ? 1000 : 2000; return iface.encodeFunctionData('depositForBurn', [ - 0n, // amount placeholder — spliced at runtime + 0n, destinationCctpDomain, mintRecipient, burnToken, - ethers.ZeroHash, // destinationCaller = anyone can complete + ethers.ZeroHash, maxFee, minFinalityThreshold, ]); } -// ─── Monolithic builder ─────────────────────────────────────────────────────── - -/** - * Builds a MonolithicExecution that: - * - Pulls inputAmount AAVE from user - * - No pre-swap fee - * - Swaps AAVE → USDC via OpenOcean (decoded return amount) - * - Takes feeAmount USDC as post-swap fee to signer - * - Splices finalAmount into depositForBurn at offset 4 - * - Approves TOKEN_MESSENGER and calls depositForBurn - */ function buildMonolithicExecution( signerAddress: string, inputAmount: bigint, @@ -171,14 +128,14 @@ function buildMonolithicExecution( return { input: { user: signerAddress, - inputToken: TOKENS.AAVE_ARB, + inputToken: TOKENS.AAVE_POLYGON, inputAmount, }, preFee: NO_FEE, swap: { target: ooRouterAddress, approvalSpender: ooRouterAddress, - outputToken: TOKENS.USDC_ARB, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, data: swapData, @@ -193,25 +150,12 @@ function buildMonolithicExecution( approvalSpender: tokenMessenger, value: 0n, data: depositForBurnData, - // amount is the first ABI param → at byte offset 4 (after 4-byte selector) amountPositions: [4n], useFinalAmountAsValue: false, }, }; } -// ─── Modular builder ────────────────────────────────────────────────────────── - -/** - * Builds an Action array: - * [0] Pull AAVE via AH.transferFrom - * [1] Approve OpenOcean router for inputAmount - * [2] Call OpenOcean router to swap AAVE → USDC - * [3] Transfer feeAmount USDC to signer - * [4] Approve TOKEN_MESSENGER for MaxUint256 (covers any USDC balance) - * [5] STATICCALL USDC.balanceOf(router) → prevReturn = 32-byte balance - * [6] Call TOKEN_MESSENGER.depositForBurn → splice prevReturn[0..32] → data[4..36] - */ function buildModularActions( signerAddress: string, routerAddress: string, @@ -226,7 +170,7 @@ function buildModularActions( 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.AAVE_ARB, + TOKENS.AAVE_POLYGON, signerAddress, routerAddress, inputAmount, @@ -234,124 +178,355 @@ function buildModularActions( const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.AAVE_ARB, encodeApprove(ooRouterAddress, inputAmount)); + exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouterAddress, inputAmount)); exec.call(ooRouterAddress, swapData); - exec.call(TOKENS.USDC_ARB, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_ARB, encodeApprove(tokenMessenger, ethers.MaxUint256)); - const balance = exec.staticCall(TOKENS.USDC_ARB, encodeBalanceOf(routerAddress)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(tokenMessenger, ethers.MaxUint256)); + const balance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(routerAddress)); exec.call(tokenMessenger, depositForBurnData).spliceArg(0, balance.returnWord()); return exec.toActions(); } -// ─── Main ───────────────────────────────────────────────────────────────────── +function buildMonolithicExecutionUsdcPolygonToBaseCctp( + signerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + depositForBurnData: string, + tokenMessenger: string, +): MonolithicExecution { + return { + input: { + user: signerAddress, + inputToken: TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + }, + preFee: { + receiver: signerAddress, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: tokenMessenger, + approvalSpender: tokenMessenger, + value: 0n, + data: depositForBurnData, + amountPositions: [4n], + useFinalAmountAsValue: false, + }, + }; +} -async function main() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } +function buildModularActionsUsdcPolygonToBaseCctp( + signerAddress: string, + routerAddress: string, + inputAmount: bigint, + feeAmount: bigint, + depositForBurnData: string, + tokenMessenger: string, +): ModularAction[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.USDC_POLYGON_CIRCLE, + signerAddress, + routerAddress, + inputAmount, + ]); - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(tokenMessenger, ethers.MaxUint256)); + const balance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(routerAddress)); + exec.call(tokenMessenger, depositForBurnData).spliceArg(0, balance.returnWord()); + return exec.toActions(); +} - const inputToken = TOKENS.AAVE_ARB; - const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( - inputToken, +async function executeLegUsdcPolygonToBaseCctp(args: { + label: string; + useModular: boolean; + signer: ethers.Wallet; + signerAddress: string; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { label, useModular, signer, signerAddress, inputAmount, routerIface } = args; + + console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + console.log(`Input USDC: ${ethers.formatUnits(inputAmount, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(inputAmount - feeAmount, 6)}`); + + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + console.log(`CCTP burn token: ${polyCctp.usdcAddress}`); + const depositForBurnData = buildDepositForBurnCalldata( signerAddress, - provider, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + true, ); - if (inputAmount === 0n) { - throw new Error( - `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with AAVE on Arbitrum first.`, - ); + + let execCalldata: string; + if (useModular) { + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + buildModularActionsUsdcPolygonToBaseCctp( + signerAddress, + ROUTER_POLYGON, + inputAmount, + feeAmount, + depositForBurnData, + polyCctp.tokenMessenger, + ), + ]); + } else { + execCalldata = routerIface.encodeFunctionData('performExecution', [ + buildMonolithicExecutionUsdcPolygonToBaseCctp( + signerAddress, + inputAmount, + feeAmount, + depositForBurnData, + polyCctp.tokenMessenger, + ), + ]); } - const arbCctp = CCTP_CONFIG[CHAIN_IDS.ARBITRUM]; - const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; - const useModular = false; - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ADDRESS}`); - console.log(`Input token: ${inputToken}`); - console.log( - `Input: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`, + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + ROUTER_POLYGON, + execCalldata, + ); + + const modeLabel = useModular ? 'Modular' : 'Monolithic'; + logTxnSummary( + `Polygon USDC → Base USDC — CCTP — ${modeLabel}`, + CHAIN_IDS.POLYGON, + receipt, ); - console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); - console.log(''); +} + +async function executeLeg(args: { + label: string; + useModular: boolean; + signer: ethers.Wallet; + signerAddress: string; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { label, useModular, signer, signerAddress, inputAmount, routerIface } = args; - // Fetch OpenOcean quote - console.log('Fetching OpenOcean swap quote (AAVE→USDC Arbitrum)...'); + console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + + console.log('Fetching OpenOcean swap quote (Polygon AAVE → Circle native USDC)...'); const { routerAddress: ooRouterAddress, swapData, minAmountOut, estimatedOut, - } = await fetchOpenOceanSwapQuote(ROUTER_ADDRESS, inputAmount); + } = await fetchOpenOceanSwapQuote(ROUTER_POLYGON, inputAmount); const feeAmount = bpsOf(estimatedOut, FEE_BPS); console.log(`OO Router: ${ooRouterAddress}`); - console.log(`Est. USDC out: ${ethers.formatUnits(estimatedOut, 6)} USDC`); - console.log( - `Post-swap fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, - ); - console.log(`Min USDC out: ${ethers.formatUnits(minAmountOut, 6)} USDC`); - console.log(''); + console.log(`Est. USDC out: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(`Post-swap fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); + console.log(`Min USDC out: ${ethers.formatUnits(minAmountOut, 6)}`); - // Build CCTP depositForBurn calldata (amount=0 placeholder, will be spliced) + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + console.log(`CCTP burn token: ${polyCctp.usdcAddress} (must match swap output)`); const depositForBurnData = buildDepositForBurnCalldata( - signerAddress, // recipient on Base - arbCctp.usdcAddress, // token being burned - baseCctp.cctpDomain, // destination domain = Base - true, // fast path + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + true, ); - const routerIface = new ethers.Interface(ROUTER_ABI); let execCalldata: string; - if (useModular) { - const actions = buildModularActions( - signerAddress, - ROUTER_ADDRESS, - inputAmount, - feeAmount, - ooRouterAddress, - swapData, - depositForBurnData, - arbCctp.tokenMessenger, - ); execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - actions, + buildModularActions( + signerAddress, + ROUTER_POLYGON, + inputAmount, + feeAmount, + ooRouterAddress, + swapData, + depositForBurnData, + polyCctp.tokenMessenger, + ), ]); - console.log('Using performModularExecution'); } else { - const exec = buildMonolithicExecution( - signerAddress, - inputAmount, - feeAmount, - minAmountOut, - ooRouterAddress, - swapData, - depositForBurnData, - arbCctp.tokenMessenger, - ); - execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); - console.log('Using performExecution (monolithic)'); + execCalldata = routerIface.encodeFunctionData('performExecution', [ + buildMonolithicExecution( + signerAddress, + inputAmount, + feeAmount, + minAmountOut, + ooRouterAddress, + swapData, + depositForBurnData, + polyCctp.tokenMessenger, + ), + ]); } - await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - console.log('Sending AllowanceHolder.exec transaction...'); + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); const receipt = await execViaAH( signer, - ROUTER_ADDRESS, - TOKENS.AAVE_ARB, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, inputAmount, - ROUTER_ADDRESS, + ROUTER_POLYGON, execCalldata, ); - console.log(`\nSuccess! Gas used: ${receipt.gasUsed.toString()}`); + const modeLabel = useModular ? 'Modular' : 'Monolithic'; + logTxnSummary( + `Polygon AAVE → Base USDC — OpenOcean + CCTP — ${modeLabel}`, + CHAIN_IDS.POLYGON, + receipt, + ); +} + +async function mainUsdcPolygonToBaseCctp() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero Circle native USDC on Polygon. Fund ${inputToken} on Polygon PoS.`, + ); + } + + const legAmount = walletBalance / 2n; + if (legAmount === 0n) { + throw new Error( + `Balance ${walletBalance} too small for two nonzero 50% legs.`, + ); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Polygon USDC: ${ethers.formatUnits(walletBalance, 6)} (full)`); + console.log(`Per leg input: ${ethers.formatUnits(legAmount, 6)} (50%)`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + await executeLegUsdcPolygonToBaseCctp({ + label: '1/2 Monolithic', + useModular: false, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log('Sleeping ~3s before modular execution...'); + await sleep(3000); + + await executeLegUsdcPolygonToBaseCctp({ + label: '2/2 Modular', + useModular: true, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`, + ); +} + +async function main() { + const cctpE2eCase = process.argv[2]?.toLowerCase(); + if (cctpE2eCase === 'usdc-polygon-base' || cctpE2eCase === 'usdc') { + await mainUsdcPolygonToBaseCctp(); + return; + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero Polygon AAVE. Fund ${inputToken} on Polygon PoS.`, + ); + } + + const legAmount = walletBalance / 2n; + if (legAmount === 0n) { + throw new Error( + `Balance ${walletBalance} too small for two nonzero 50% legs.`, + ); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Polygon AAVE: ${ethers.formatUnits(walletBalance, 18)} (full)`); + console.log(`Per leg input: ${ethers.formatUnits(legAmount, 18)} (50%)`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + await executeLeg({ + label: '1/2 Monolithic', + useModular: false, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + + console.log('Sleeping ~3s before modular execution...'); + await sleep(3000); + + await executeLeg({ + label: '2/2 Modular', + useModular: true, + signer, + signerAddress, + inputAmount: legAmount, + routerIface, + }); + console.log( - `USDC will arrive on Base at ${signerAddress} after CCTP attestation.`, + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`, ); } diff --git a/scripts/e2e/swapBridgeViaOft.ts b/scripts/e2e/swapBridgeViaOft.ts new file mode 100644 index 0000000..f9fe823 --- /dev/null +++ b/scripts/e2e/swapBridgeViaOft.ts @@ -0,0 +1,785 @@ +/** + * Script — Swap AAVE Polygon → USDT0 Polygon, then bridge to Arbitrum USDT via USDT0 OFT (LayerZero v2) + * + * Two independent scenarios run back-to-back (monolithic + modular each): + * + * Case 1 — AAVE Polygon → USDT0 Polygon (OpenOcean) → Arbitrum USDT (USDT0 OFT bridge) + * 1. OpenOcean swap_quote: AAVE → USDT0 on Polygon (router is sender + recipient of swap) + * 2. Approve AllowanceHolder (0x AH) for the AAVE input amount + * 3. Post-swap fee: FEE_BPS of the OpenOcean USDT0 output amount is transferred to signer EOA + * 4. OFT quote: quoteSend + quoteOFT on the USDT0 OFT Adapter (Polygon) to get LZ nativeFee + amountReceivedLD + * 5. Build send() calldata: amountLD = 0 placeholder, spliced at runtime (byte offset 196) + * 6. Execute via AllowanceHolder.exec(); msg.value = nativeFeeWithBuffer (5% buffer on LZ fee) + * + * Case 2 — USDT0 Polygon → Arbitrum USDT (direct OFT bridge, no swap) + * 1. Pre-bridge fee: FEE_BPS of input USDT0 transferred to signer EOA + * 2. OFT quote + send() calldata (same as above) + * 3. Execute; msg.value = nativeFeeWithBuffer + * + * OFT mechanics (Polygon USDT0 uses OFT_ADAPTER — approval required): + * - Call quoteSend() + quoteOFT() on USDT0_OFT_ADAPTER_POLYGON (dstEid = ARBITRUM_LZ_EID 30110) + * - Approve the OFT Adapter to spend TOKENS.USDT0_POLYGON before calling send() + * - Pass nativeFeeWithBuffer as msg.value (POL on Polygon) so the router forwards LZ fee to the adapter + * - amountLD in send() is spliced at byte offset 196 from the actual post-fee token balance + * + * sendParam.amountLD offset derivation (same as Stargate): + * ABI layout after 4-byte selector: + * sendParam_ptr (32) | fee.nativeFee (32) | fee.lzTokenFee (32) | refundAddress (32) | tail... + * Tail (sendParam body): + * dstEid (32) | to (32) | amountLD (32) ← byte 4 + 3*32 + 2*32 = 196 from calldata start + * + * LZ extraOptions for USDT0 OFT (addExecutorLzReceiveOption(65000, 0)): + * Generated at runtime via @layerzerolabs/lz-v2-utilities Options SDK. + * Equivalent to: type3(0x0003) | workerId(0x01) | optLen(0x0011) | optType(0x01) | uint128(65000) + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaOft.ts all + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaOft.ts aave-usdt0-oft + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaOft.ts usdt0-direct + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + ALLOWANCE_HOLDER, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from './config'; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from './utils/allowanceHolder'; +import { + encodeApprove, + encodeTransfer, + encodeBalanceOf, + getWalletErc20Balance, +} from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; +import type { ModularAction } from './utils/modularActionsBuilder/index'; +import { MonolithicExecution, NO_FEE, NO_SWAP } from './utils/contractTypes'; +import { sleep } from './utils/sleep'; +import { logTxnSummary } from './utils/txnLogSummary'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +// ─── Constants ──────────────────────────────────────────────────────────────── + +/** Byte offset of sendParam.amountLD within the OFT send() calldata (same as Stargate). */ +const OFT_AMOUNT_LD_OFFSET = 196; + +/** + * LZ executor options for the OFT bridge: TYPE_3 + addExecutorLzReceiveOption(gas=65000, value=0). + * Generated via the @layerzerolabs/lz-v2-utilities SDK (same as oft.service.ts in bungee-backend). + */ +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); + +// ─── OFT ABI ───────────────────────────────────────────────────────────────── + +/** Minimal OFT / OFT Adapter ABI for quoting and sending. */ +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +// ─── OpenOcean quote ────────────────────────────────────────────────────────── + +interface OoSwapQuoteResponse { + data: { + to: string; + data: string; + value: string; + outAmount: string; + minOutAmount: string; + }; +} + +/** + * Fetches an OpenOcean swap_quote for AAVE → USDT0 on Polygon. + * The router address is used as both sender and account so tokens land in the router. + */ +async function fetchOpenOceanQuote( + inputAmount: bigint, + slippageBps: number = 100, +): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), // AAVE has 18 decimals + slippage: (slippageBps / 100).toString(), + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) { + params.apikey = OPEN_OCEAN_API_KEY; + } + + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +// ─── OFT quote ──────────────────────────────────────────────────────────────── + +interface OftQuoteResult { + nativeFee: bigint; + nativeFeeWithBuffer: bigint; + amountReceivedLD: bigint; +} + +/** + * Fetches the LZ nativeFee and expected received amount from the USDT0 OFT Adapter on Polygon. + * + * @param provider JSON-RPC provider for Polygon + * @param bridgeAmountLD Amount of USDT0 (6 decimals on Polygon) to bridge + * @param recipient Recipient address on Arbitrum (also used as refundAddress) + */ +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider, + ); + const to32 = ethers.zeroPadValue(recipient, 32); + + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }; + + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + + const nativeFee = fee.nativeFee as bigint; + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; // 5% buffer + + return { + nativeFee, + nativeFeeWithBuffer, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +// ─── OFT send() calldata builder ───────────────────────────────────────────── + +/** + * Encodes the OFT Adapter send() calldata. + * amountLD is set to 0 as a placeholder — the router splices the actual amount + * at byte offset 196 from the router's post-fee token balance at execution time. + * + * @param nativeFee LZ fee in POL (with 5% buffer already applied) + * @param recipient Recipient on Arbitrum (also used as refundAddress) + */ +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, // refundAddress + ]); +} + +// ─── Case 1: AAVE → USDT0 (OpenOcean swap) → USDT0 Base (OFT bridge) ───────── + +/** + * Monolithic for Case 1: + * - Swap AAVE → USDT0 via OpenOcean (swap step) + * - Post-swap fee: FEE_BPS of estimated USDT0 output transferred to signer + * - Bridge remaining USDT0 via OFT Adapter (approval required) + * - useFinalAmountAsValue=false; amountPositions=[196n] splices actual balance into amountLD + * - bridge.value = nativeFeeWithBuffer (forwarded as LZ msg.value) + */ +function buildCase1Monolithic( + signer: string, + inputAmount: bigint, + feeAmount: bigint, + minAmountOut: bigint, + ooRouter: string, + swapData: string, + oftSendData: string, + nativeFeeWithBuffer: bigint, +): MonolithicExecution { + return { + input: { + user: signer, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount, + }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + data: swapData, + returnDataWordOffset: 0n, + }, + postFee: { + receiver: signer, + amount: feeAmount, + }, + bridge: { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, // adapter needs ERC-20 approval + value: nativeFeeWithBuffer, // forwarded as LZ native fee + data: oftSendData, + amountPositions: [BigInt(OFT_AMOUNT_LD_OFFSET)], // splice actual USDT0 balance at byte 196 + useFinalAmountAsValue: false, + }, + }; +} + +/** + * Modular for Case 1: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] ooRouter swap calldata — AAVE → USDT0 lands in router + * [3] USDT0.transfer(signer, feeAmount) — post-swap fee to signer + * [4] USDT0.approve(adapter, MaxUint256) — allow adapter to pull USDT0 + * [5] STATICCALL USDT0.balanceOf(router) — capture post-fee balance + * [6] nativeCall adapter.send(...) — spliceWord patches amountLD from [5] + */ +function buildCase1Modular( + signer: string, + inputAmount: bigint, + feeAmount: bigint, + ooRouter: string, + swapData: string, + oftSendData: string, + nativeFeeWithBuffer: bigint, +): ModularAction[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.AAVE_POLYGON, + signer, + ROUTER_POLYGON, + inputAmount, + ]); + + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); // AAVE → USDT0 lands in router + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signer, feeAmount)); // post-swap fee + exec.call( + TOKENS.USDT0_POLYGON, + encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256), + ); + const usdt0Balance = exec.staticCall( + TOKENS.USDT0_POLYGON, + encodeBalanceOf(ROUTER_POLYGON), + ); + exec + .nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + return exec.toActions(); +} + +async function executeCase1Leg(args: { + label: string; + useModular: boolean; + signer: ethers.Wallet; + signerAddress: string; + provider: ethers.JsonRpcProvider; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { + label, + useModular, + signer, + signerAddress, + provider, + inputAmount, + routerIface, + } = args; + console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + + console.log('Fetching OpenOcean quote (Polygon AAVE → USDT0)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0 out: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-swap fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`, + ); + console.log(` Min USDT0 out: ${ethers.formatUnits(minAmountOut, 6)}`); + console.log(` Bridge amount: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching USDT0 OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress, + ); + + console.log( + ` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} POL`, + ); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + let execCalldata: string; + if (useModular) { + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + buildCase1Modular( + signerAddress, + inputAmount, + feeAmount, + ooRouter, + swapData, + oftSendData, + nativeFeeWithBuffer, + ), + ]); + } else { + execCalldata = routerIface.encodeFunctionData('performExecution', [ + buildCase1Monolithic( + signerAddress, + inputAmount, + feeAmount, + minAmountOut, + ooRouter, + swapData, + oftSendData, + nativeFeeWithBuffer, + ), + ]); + } + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + + console.log( + `AllowanceHolder.exec (txValue = ${ethers.formatEther( + nativeFeeWithBuffer, + )} ETH)...`, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary( + `Arbitrum AAVE → USDT (OO swap) → Arbitrum USDT0 (OFT) — ${ + useModular ? 'Modular' : 'Monolithic' + }`, + CHAIN_IDS.POLYGON, + receipt, + ); +} + +// ─── Case 2: Arbitrum USDT → Base USDT0 (direct OFT bridge, no swap) ───────── + +/** + * Monolithic for Case 2: + * - No swap (NO_SWAP) + * - Pre-bridge fee: FEE_BPS of input USDT0 transferred to signer + * - Bridge remaining USDT0 via OFT Adapter (approval required) + * - useFinalAmountAsValue=false; amountPositions=[196n] splices actual balance + * - bridge.value = nativeFeeWithBuffer (forwarded as LZ msg.value) + */ +function buildCase2Monolithic( + signer: string, + inputAmount: bigint, + feeAmount: bigint, + oftSendData: string, + nativeFeeWithBuffer: bigint, +): MonolithicExecution { + return { + input: { + user: signer, + inputToken: TOKENS.USDT0_POLYGON, + inputAmount, + }, + preFee: { + receiver: signer, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + data: oftSendData, + amountPositions: [BigInt(OFT_AMOUNT_LD_OFFSET)], + useFinalAmountAsValue: false, + }, + }; +} + +/** + * Modular for Case 2: + * [0] AH.transferFrom(USDT0, signer, router, inputAmount) + * [1] USDT0.transfer(signer, feeAmount) — pre-bridge fee to signer + * [2] USDT0.approve(adapter, MaxUint256) — allow adapter to pull USDT0 + * [3] STATICCALL USDT0.balanceOf(router) — capture post-fee balance + * [4] nativeCall adapter.send(...) — spliceWord patches amountLD from [3] + */ +function buildCase2Modular( + signer: string, + inputAmount: bigint, + feeAmount: bigint, + oftSendData: string, + nativeFeeWithBuffer: bigint, +): ModularAction[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ + TOKENS.USDT0_POLYGON, + signer, + ROUTER_POLYGON, + inputAmount, + ]); + + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahTransferFromData); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signer, feeAmount)); // pre-bridge fee + exec.call( + TOKENS.USDT0_POLYGON, + encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256), + ); + const usdt0Balance = exec.staticCall( + TOKENS.USDT0_POLYGON, + encodeBalanceOf(ROUTER_POLYGON), + ); + exec + .nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + return exec.toActions(); +} + +async function executeCase2Leg(args: { + label: string; + useModular: boolean; + signer: ethers.Wallet; + signerAddress: string; + provider: ethers.JsonRpcProvider; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { + label, + useModular, + signer, + signerAddress, + provider, + inputAmount, + routerIface, + } = args; + console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(` Input USDT0: ${ethers.formatUnits(inputAmount, 6)}`); + console.log( + ` Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`, + ); + console.log(` Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching USDT0 OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress, + ); + + console.log( + ` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} POL`, + ); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + let execCalldata: string; + if (useModular) { + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + buildCase2Modular( + signerAddress, + inputAmount, + feeAmount, + oftSendData, + nativeFeeWithBuffer, + ), + ]); + } else { + execCalldata = routerIface.encodeFunctionData('performExecution', [ + buildCase2Monolithic( + signerAddress, + inputAmount, + feeAmount, + oftSendData, + nativeFeeWithBuffer, + ), + ]); + } + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.USDT0_POLYGON, + inputAmount, + ); + + console.log( + `AllowanceHolder.exec (txValue = ${ethers.formatEther( + nativeFeeWithBuffer, + )} ETH)...`, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary( + `Polygon USDT → Arbitrum USDT0 (OFT direct) — ${ + useModular ? 'Modular' : 'Monolithic' + }`, + CHAIN_IDS.POLYGON, + receipt, + ); +} + +// ─── Case runners ───────────────────────────────────────────────────────────── + +async function runCase1( + signer: ethers.Wallet, + signerAddress: string, + routerIface: ethers.Interface, +): Promise { + console.log(`\n${'═'.repeat(70)}`); + console.log( + 'CASE 1: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (OFT bridge)', + ); + console.log('═'.repeat(70)); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signerOnChain = signer.connect(provider); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Case 1: signer ${signerAddress} has zero AAVE on Polygon. Fund ${TOKENS.AAVE_POLYGON}.`, + ); + } + + const legAmount = walletBalance / 2n; + if (legAmount === 0n) { + throw new Error('Case 1: AAVE balance too small to split into two halves.'); + } + + console.log( + `Input token (AAVE): ${ethers.formatUnits( + walletBalance, + 18, + )} (full balance)`, + ); + console.log(`Per leg: ${ethers.formatUnits(legAmount, 18)}`); + + await executeCase1Leg({ + label: '1/2', + useModular: false, + signer: signerOnChain, + signerAddress, + provider, + inputAmount: legAmount, + routerIface, + }); + + console.log('\nSleeping 3s before modular leg...'); + await sleep(3000); + + await executeCase1Leg({ + label: '2/2', + useModular: true, + signer: signerOnChain, + signerAddress, + provider, + inputAmount: legAmount, + routerIface, + }); +} + +async function runCase2( + signer: ethers.Wallet, + signerAddress: string, + routerIface: ethers.Interface, +): Promise { + console.log(`\n${'═'.repeat(70)}`); + console.log( + 'CASE 2: Polygon USDT0 → Arbitrum USDT0 (direct OFT bridge, no swap)', + ); + console.log('═'.repeat(70)); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signerOnChain = signer.connect(provider); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDT0_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Case 2: signer ${signerAddress} has zero USDT0 on Polygon. Fund ${TOKENS.USDT0_POLYGON}.`, + ); + } + + const legAmount = walletBalance / 2n; + if (legAmount === 0n) { + throw new Error( + 'Case 2: USDT0 balance too small to split into two halves.', + ); + } + + console.log( + `Input token (USDT0): ${ethers.formatUnits( + walletBalance, + 6, + )} (full balance)`, + ); + console.log(`Per leg: ${ethers.formatUnits(legAmount, 6)}`); + + await executeCase2Leg({ + label: '1/2', + useModular: false, + signer: signerOnChain, + signerAddress, + provider, + inputAmount: legAmount, + routerIface, + }); + + console.log('\nSleeping 3s before modular leg...'); + await sleep(3000); + + await executeCase2Leg({ + label: '2/2', + useModular: true, + signer: signerOnChain, + signerAddress, + provider, + inputAmount: legAmount, + routerIface, + }); +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const signer = new ethers.Wallet(privateKey); + const signerAddress = await signer.getAddress(); + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + + const caseArg = process.argv[2]?.toLowerCase(); + + if (caseArg === 'usdt0-direct') { + await runCase2(signer, signerAddress, routerIface); + console.log( + '\nCase 2 complete — USDT0 arrives on Arbitrum once LZ delivers the message.', + ); + return; + } + if (caseArg === 'aave-usdt0-oft') { + await runCase1(signer, signerAddress, routerIface); + console.log( + '\nCase 1 complete — USDT0 arrives on Arbitrum once LZ delivers the message.', + ); + return; + } + + console.error( + `Unknown case: ${caseArg}. Use: all | aave-usdt0-oft | usdt0-direct`, + ); + process.exit(1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index 86b9f66..08671e9 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -1,33 +1,39 @@ /** - * Script 4 — Swap USDC Arbitrum → native ETH Arbitrum via OpenOcean, - * then bridge ETH Arbitrum → ETH Base via Stargate Native Pool + * Stargate e2e test script — three independent cases, each running a + * monolithic leg followed (after a 3-second pause) by a modular leg. * - * Flow: - * 1. Fetch the signer's full USDC balance on Arbitrum as input. - * 2. Fetch an OpenOcean swap_quote for USDC → native ETH on Arbitrum. - * 3. Call Stargate quoteSend to get the LayerZero nativeFee. - * 4. Pre-compute amountLD = estimatedFinalAmount - nativeFee for the Stargate calldata. - * 5. Build either a monolithic or modular execution payload: - * - Monolithic: pull USDC via AH → swap USDC→ETH → post-fee (ETH to signer) → - * Stargate send (useFinalAmountAsValue=true, static amountLD) - * - Modular: pull USDC → approve OO → swap → ETH fee transfer → - * Stargate send via CALL_WITH_NATIVE with static amountLD/msg.value - * 6. Ensure AllowanceHolder ERC20 allowance for USDC. - * 7. Execute AllowanceHolder.exec, forwarding nativeFee as msg.value so the - * router has enough ETH to cover both the bridge amount and the LZ fee. + * Case 1 Arbitrum USDC → OO swap → native ETH → Stargate Native ETH Pool → Base ETH + * Case 2 Polygon USDC → (no swap) → Stargate USDC Pool → Base USDC + * Case 3 Base USDC → OO swap → native ETH → Stargate Native ETH Pool → Arb ETH * - * Stargate Native Pool design notes: - * Stargate's send() requires msg.value = amountLD + nativeFee. We pre-encode - * amountLD = estimatedFinalAmount - nativeFee in the calldata (static — no splice). - * The caller provides nativeFee as msg.value to AH.exec; this is forwarded to the - * router alongside the USDC. After the swap and fee deduction, the router's ETH - * balance = actualFinalAmount + nativeFee, which is always >= amountLD + nativeFee - * as long as the actual swap output >= the OO minimum (guaranteed by slippage). - * Any excess ETH is refunded to the signer by Stargate via refundAddress. + * Native-pool mechanics (cases 1 & 3): + * send() requires msg.value >= amountLD + nativeFee (StargatePoolNative._assertMessagingFee). + * Monolithic: useFinalAmountAsValue=true (router forwards actualFinalAmount as msg.value). + * amountLD = minAmountOut - fee - nativeFeeWithBuffer; positions=[]. + * Since actual >= min (OO slippage), msg.value >= amountLD + nativeFeeWithBuffer ✓ + * Modular: amountLD = minAmountOut - fee - nativeFeeWithBuffer (same). + * nativeCall Stargate with value = amountLD + nativeFeeWithBuffer = minAmountOut - fee. + * + * ERC20-pool mechanics (case 2): + * send() uses ERC20 transferFrom for USDC; msg.value = nativeFee only. + * Monolithic: useFinalAmountAsValue=false, amountPositions=[196n], bridge.value=nativeFeeWithBuffer. + * Modular: staticCall USDC.balanceOf(router) → spliceWord(196n) into Stargate calldata. + * nativeCall Stargate with value = nativeFeeWithBuffer. + * + * Case selection (required) — same idea as `bridgeViaRelay.ts` / `swapBridgeViaCctp.ts`: + * Pass a scenario as the first CLI arg, or set `STARGATE_E2E_CASE` when your runner + * cannot pass argv. + * + * 1 / arb-usdc-base-eth Arbitrum USDC → OO → native ETH → Stargate native → Base ETH + * 2 / polygon-usdc-base Polygon USDC → Stargate USDC pool → Base USDC (no swap) + * 3 / base-usdc-arb-eth Base USDC → OO → native ETH → Stargate native → Arbitrum ETH * * Usage: - * ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts - * USE_MODULAR=true ROUTER_ADDRESS=0x... PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth + * STARGATE_E2E_CASE=2 PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts + * + * Router per source chain: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(chainId)` in config.ts. + * Override with `ROUTER_CHAIN_` env when needed. */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -36,7 +42,7 @@ dotenv.config(); import { CHAIN_IDS, - ROUTER_ADDRESS, + routerAddressForChain, TOKENS, FEE_BPS, bpsOf, @@ -45,457 +51,622 @@ import { ALLOWANCE_HOLDER, NATIVE_TOKEN_ADDRESS, STARGATE_NATIVE_ARB, + STARGATE_NATIVE_BASE, + STARGATE_USDC_POLYGON, BASE_LZ_EID, + ARBITRUM_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, } from './config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; +import { + encodeApprove, + encodeTransfer, + encodeBalanceOf, + getWalletErc20Balance, +} from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - MonolithicExecution, - NO_FEE, - ZERO_ADDRESS, -} from './utils/contractTypes'; +import { MonolithicExecution, NO_FEE, NO_SWAP, ZERO_ADDRESS } from './utils/contractTypes'; +import { sleep } from './utils/sleep'; +import { logTxnSummary } from './utils/txnLogSummary'; + +// ─── Case configuration ─────────────────────────────────────────────────────── + +/** + * Describes a Stargate test case. `ooSwap` being null means case 2 (no swap — input + * token goes directly to Stargate). `isNativePool` drives the bridge mechanics. + */ +interface OoSwapConfig { + inToken: string; + outToken: string; + inDecimals: number; + chainId: number; + gasPrice: string; +} + +interface CaseConfig { + name: string; + sourceChainId: number; + rpc: string; + inputToken: string; + inputDecimals: number; + ooSwap: OoSwapConfig | null; // null → skip OO swap, bridge input token directly + stargatePool: string; + isNativePool: boolean; + destLzEid: number; +} + +const CASES: CaseConfig[] = [ + { + name: 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate Native Pool)', + sourceChainId: CHAIN_IDS.ARBITRUM, + rpc: RPC.ARBITRUM, + inputToken: TOKENS.USDC_ARB, + inputDecimals: 6, + ooSwap: { + inToken: TOKENS.USDC_ARB, + outToken: NATIVE_TOKEN_ADDRESS, + inDecimals: 6, + chainId: CHAIN_IDS.ARBITRUM, + gasPrice: '1', + }, + stargatePool: STARGATE_NATIVE_ARB, + isNativePool: true, + destLzEid: BASE_LZ_EID, + }, + { + name: 'Polygon USDC → Base USDC (Stargate USDC Pool, no swap)', + sourceChainId: CHAIN_IDS.POLYGON, + rpc: RPC.POLYGON, + inputToken: TOKENS.USDC_POLYGON_CIRCLE, + inputDecimals: 6, + ooSwap: null, // skip OO swap — bridge USDC directly + stargatePool: STARGATE_USDC_POLYGON, + isNativePool: false, + destLzEid: BASE_LZ_EID, + }, + { + name: 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate Native Pool)', + sourceChainId: CHAIN_IDS.BASE, + rpc: RPC.BASE, + inputToken: TOKENS.USDC_BASE, + inputDecimals: 6, + ooSwap: { + inToken: TOKENS.USDC_BASE, + outToken: NATIVE_TOKEN_ADDRESS, + inDecimals: 6, + chainId: CHAIN_IDS.BASE, + gasPrice: '1', + }, + stargatePool: STARGATE_NATIVE_BASE, + isNativePool: true, + destLzEid: ARBITRUM_LZ_EID, + }, +]; + +/** Slug aliases (and `1`/`2`/`3`) → index in `CASES`. */ +const STARGATE_SCENARIO_ALIASES: Record = { + '1': 0, + 'arb-usdc-base-eth': 0, + 'arb-native-base': 0, + 'arbitrum-usdc-base-eth': 0, -// ─── Stargate ABI ───────────────────────────────────────────────────────────── + '2': 1, + 'polygon-usdc-base': 1, + 'usdc-polygon-base': 1, + + '3': 2, + 'base-usdc-arb-eth': 2, + 'base-native-arb': 2, +}; /** - * Minimal Stargate OFT/Native pool ABI fragments needed for quoting and bridging. - * The SendParam struct and MessagingFee struct are encoded inline as tuples. + * Resolves scenario from CLI (`process.argv[2]`) or `STARGATE_E2E_CASE`, then + * returns the matching `CaseConfig`. Fails fast with a usage message if unset/unknown. */ +function resolveScenarioConfig(): CaseConfig { + const raw = (process.argv[2] ?? process.env.STARGATE_E2E_CASE ?? '').trim().toLowerCase(); + if (!raw) { + console.error( + 'Missing scenario. Pass argv[2] or set STARGATE_E2E_CASE. Examples:\n' + + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth\n' + + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-usdc-base\n' + + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts base-usdc-arb-eth\n' + + 'Or use numeric slugs 1 | 2 | 3.', + ); + process.exit(1); + } + const idx = STARGATE_SCENARIO_ALIASES[raw]; + if (idx === undefined || !CASES[idx]) { + console.error(`Unknown Stargate e2e scenario "${raw}". Valid: ${Object.keys(STARGATE_SCENARIO_ALIASES).sort().join(', ')}`); + process.exit(1); + } + return CASES[idx]; +} + +// ─── Shared Stargate ABI ────────────────────────────────────────────────────── + +/** Minimal Stargate pool ABI fragments — identical for native and ERC20 pools. */ const STARGATE_ABI = [ - // Quote the LayerZero fee for a given SendParam 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', - // Query OFT limits and expected receive amounts (for informational logging) 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', - // Execute the bridge transfer; msg.value = amountLD + nativeFee 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', ]; -// ─── OpenOcean swap quote ───────────────────────────────────────────────────── +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); -interface OpenOceanSwapQuoteResponse { +// ─── OpenOcean quote ────────────────────────────────────────────────────────── + +interface OoQuoteResponse { data: { to: string; data: string; - value: string; - estimatedGas: string; outAmount: string; minOutAmount: string; }; } /** - * Fetches a swap_quote from OpenOcean for USDC → native ETH on Arbitrum. - * The router is used as both sender and account so the swap output lands in the router. - * - * @param routerAddress Address that will execute the swap (receives ETH output) - * @param inputAmount Amount of USDC in base units (6 decimals) - * @param slippageBps Slippage tolerance in basis points (e.g. 100 = 1%) + * Fetches an OpenOcean swap_quote. + * `amount` is in the input token's native units (raw bigint). */ -async function fetchOpenOceanSwapQuote( +async function fetchOoQuote( + cfg: OoSwapConfig, routerAddress: string, - inputAmount: bigint, + amount: bigint, slippageBps: number = 100, -): Promise<{ - routerAddress: string; - swapData: string; - minAmountOut: bigint; - estimatedOut: bigint; -}> { +): Promise<{ ooRouter: string; swapData: string; estimatedOut: bigint; minAmountOut: bigint }> { const params: Record = { - inTokenAddress: TOKENS.USDC_ARB, - // OpenOcean uses the canonical ETH sentinel for native output - outTokenAddress: NATIVE_TOKEN_ADDRESS, - amount: ethers.formatUnits(inputAmount, 6), // USDC has 6 decimals + inTokenAddress: cfg.inToken, + outTokenAddress: cfg.outToken, + amount: ethers.formatUnits(amount, cfg.inDecimals), slippage: (slippageBps / 100).toString(), - // sender = account = router so the swap executes from and into the router sender: routerAddress, account: routerAddress, - gasPrice: '1', // gwei; does not affect routing + gasPrice: cfg.gasPrice, }; if (OPEN_OCEAN_API_KEY) { - params['apikey'] = OPEN_OCEAN_API_KEY; + params.apikey = OPEN_OCEAN_API_KEY; } - - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; - const response = await axios.get(url, { params }); + const url = `https://open-api.openocean.finance/v3/${cfg.chainId}/swap_quote`; + const response = await axios.get(url, { params }); const q = response.data.data; - return { - routerAddress: q.to, + ooRouter: q.to, swapData: q.data, - minAmountOut: BigInt(q.minOutAmount), estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), }; } // ─── Stargate quote ─────────────────────────────────────────────────────────── /** - * Builds the Stargate SendParam and fetches both quoteSend (nativeFee) and - * quoteOFT (expected receive amount on Base) in parallel. + * Fetches the LZ nativeFee and expected receive amount from Stargate. * - * @param provider JSON-RPC provider connected to Arbitrum - * @param recipientAddress Recipient address on Base (refundAddress for excess) - * @param bridgeAmountLD Tentative amountLD for the quote (wei) + * @param pool Pool contract address on the source chain + * @param provider Provider for the source chain + * @param destLzEid LayerZero destination EID + * @param recipient Recipient on destination (also refundAddress) + * @param bridgeAmountLD Tentative bridge amount for the quote */ async function fetchStargateQuote( + pool: string, provider: ethers.JsonRpcProvider, - recipientAddress: string, + destLzEid: number, + recipient: string, bridgeAmountLD: bigint, -): Promise<{ - nativeFee: bigint; - amountReceivedLD: bigint; -}> { - const stargate = new ethers.Contract(STARGATE_NATIVE_ARB, STARGATE_ABI, provider); - - // Stargate uses bytes32-padded address for `to` - const recipientBytes32 = ethers.zeroPadValue(recipientAddress, 32); - +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(pool, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); const sendParam = { - dstEid: BASE_LZ_EID, - to: recipientBytes32, + dstEid: destLzEid, + to: to32, amountLD: bridgeAmountLD, - // minAmountLD for quoting: use 0 so the quote always succeeds minAmountLD: 0n, - extraOptions: '0x', // Stargate native pools use empty extraOptions + extraOptions: '0x', composeMsg: '0x', oftCmd: '0x', }; - - const [messagingFee, oftQuote] = await Promise.all([ - stargate.quoteSend(sendParam, false), // payInLzToken=false → native fee - stargate.quoteOFT(sendParam), + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), ]); - return { - nativeFee: messagingFee.nativeFee as bigint, - amountReceivedLD: (oftQuote.oftReceipt.amountReceivedLD as bigint), + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, }; } -// ─── Stargate send() calldata ───────────────────────────────────────────────── +// ─── Stargate calldata builder ──────────────────────────────────────────────── /** - * Encodes the Stargate send() calldata. + * Encodes Stargate send() calldata. * - * amountLD is the exact amount passed to Stargate. Stargate's Native pool - * requires msg.value = amountLD + nativeFee; the caller must forward the total. + * For native pools: pass a pre-computed amountLD; no splice required. + * For ERC20 pools: pass amountLD=0 as a placeholder; caller splices the + * real amount at STARGATE_AMOUNT_LD_OFFSET (196 bytes). * - * @param amountLD Amount of ETH to bridge (wei); static — no splice needed - * @param nativeFee LayerZero messaging fee (wei) - * @param recipientAddress Recipient on Base (also the refundAddress for excess ETH) + * @param destLzEid Destination LZ endpoint ID + * @param nativeFee LZ fee in source-chain native token (with buffer) + * @param recipient Recipient address on destination chain + * @param amountLD Explicit amountLD (for native pools); 0n for ERC20 pools */ function buildStargateCalldata( - amountLD: bigint, + destLzEid: number, nativeFee: bigint, - recipientAddress: string, + recipient: string, + amountLD: bigint, ): string { - const stargateIface = new ethers.Interface(STARGATE_ABI); - const recipientBytes32 = ethers.zeroPadValue(recipientAddress, 32); - - return stargateIface.encodeFunctionData('send', [ + return STARGATE_IFACE.encodeFunctionData('send', [ { - dstEid: BASE_LZ_EID, - to: recipientBytes32, + dstEid: destLzEid, + to: ethers.zeroPadValue(recipient, 32), amountLD, - minAmountLD: 0n, // accept any amount received on destination (e2e testing) + minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x', }, - { - nativeFee, - lzTokenFee: 0n, - }, - recipientAddress, // refundAddress: excess ETH (if amountLD < msg.value - nativeFee) goes here + { nativeFee, lzTokenFee: 0n }, + recipient, // refundAddress ]); } -// ─── Monolithic builder ─────────────────────────────────────────────────────── +// ─── Monolithic builders ────────────────────────────────────────────────────── /** - * Builds a MonolithicExecution struct for: - * pull USDC → swap USDC→ETH (OO) → post-fee ETH to signer → Stargate send - * - * Bridge design: - * - amountLD is pre-encoded in stargateData as (minAmountOut - feeAmount - nativeFeeWithBuffer). - * - useFinalAmountAsValue=true forwards the actual post-fee ETH (finalAmount) as msg.value. - * - Because finalAmount >= minAmountOut - feeAmount = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee, - * the Stargate msg.value check always passes regardless of swap slippage. - * - Any excess ETH (finalAmount - amountLD - nativeFee) is refunded by Stargate to refundAddress. - * - * @param signerAddress Signer/recipient address - * @param inputAmount USDC amount in base units - * @param feeAmount Post-swap fee in wei (ETH) - * @param minAmountOut Minimum ETH from swap (wei); swap reverts if output < this - * @param ooRouterAddress OpenOcean router address returned by the quote - * @param swapData OpenOcean swap calldata - * @param stargateData Pre-built Stargate send() calldata + * Monolithic for native-pool cases (cases 1 & 3): + * - OO swap input token → native ETH + * - useFinalAmountAsValue=true: router forwards actualFinalETH as msg.value to Stargate + * - amountLD = minAmountOut - fee - nativeFeeWithBuffer; pre-encoded; no splice needed (positions=[]) + * - StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= min */ -function buildMonolithicExecution( - signerAddress: string, +function buildNativePoolMonolithic( + signer: string, + cfg: CaseConfig, inputAmount: bigint, feeAmount: bigint, minAmountOut: bigint, - ooRouterAddress: string, + ooRouter: string, swapData: string, stargateData: string, ): MonolithicExecution { return { - input: { - user: signerAddress, - inputToken: TOKENS.USDC_ARB, - inputAmount, - }, + input: { user: signer, inputToken: cfg.inputToken, inputAmount }, preFee: NO_FEE, swap: { - target: ooRouterAddress, - approvalSpender: ooRouterAddress, - outputToken: NATIVE_TOKEN_ADDRESS, // ETH out + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, data: swapData, returnDataWordOffset: 0n, }, - postFee: { - receiver: signerAddress, - amount: feeAmount, + postFee: { receiver: signer, amount: feeAmount }, + bridge: { + target: cfg.stargatePool, + approvalSpender: ZERO_ADDRESS, // no ERC20 approval for native ETH + value: 0n, // ignored when useFinalAmountAsValue=true + data: stargateData, + amountPositions: [], // amountLD is pre-encoded + useFinalAmountAsValue: true, // forward actualFinalETH as msg.value }, + }; +} + +/** + * Monolithic for ERC20-pool case (case 2): + * - No OO swap (NO_SWAP) — input USDC goes directly to bridge + * - useFinalAmountAsValue=false: USDC transferred via ERC20 approval + * - amountPositions=[196n]: router splices finalAmount into amountLD at runtime + * - bridge.value=nativeFeeWithBuffer: forwarded as msg.value for the LZ fee + */ +function buildErc20PoolMonolithic( + signer: string, + cfg: CaseConfig, + inputAmount: bigint, + feeAmount: bigint, + stargateData: string, + nativeFeeWithBuffer: bigint, +): MonolithicExecution { + return { + input: { user: signer, inputToken: cfg.inputToken, inputAmount }, + preFee: NO_FEE, + swap: NO_SWAP, // skip swap — finalToken = inputToken, finalAmount = inputAmount - preFee + postFee: { receiver: signer, amount: feeAmount }, bridge: { - target: STARGATE_NATIVE_ARB, - approvalSpender: ZERO_ADDRESS, // native ETH — no ERC20 approval needed - value: 0n, + target: cfg.stargatePool, + approvalSpender: cfg.stargatePool, // router must approve USDC to pool + value: nativeFeeWithBuffer, // POL/native forwarded as LZ fee msg.value data: stargateData, - // amountLD is pre-encoded in stargateData; no runtime splice needed - amountPositions: [], - // Router forwards actualFinalAmount as msg.value to Stargate. - // Caller ensures nativeFee is included in AH.exec msg.value so that - // router's ETH balance = actualFinalAmount + nativeFee at bridge time. - useFinalAmountAsValue: true, + amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice at byte 196 + useFinalAmountAsValue: false, }, }; } -// ─── Modular builder ────────────────────────────────────────────────────────── +// ─── Modular builders ───────────────────────────────────────────────────────── /** - * Builds the Action array for modular execution: - * [0] AH.transferFrom USDC (pull from user) - * [1] USDC.approve(ooRouter, inputAmount) - * [2] Call OO router to swap USDC → ETH - * [3] Send feeAmount ETH to signer via CALL_WITH_NATIVE - * [4] Stargate send() via CALL_WITH_NATIVE - * - * amountLD in the calldata and msg.value are pre-encoded and do not need splicing. + * Modular for native-pool cases (cases 1 & 3): + * [0] AH.transferFrom input token + * [1] approve(ooRouter, inputAmount) + * [2] OO swap → native ETH lands in router + * [3] nativeCall: send fee ETH to signer + * [4] nativeCall: Stargate send() with value = amountLD + nativeFeeWithBuffer = minAmountOut - fee * - * @param signerAddress Signer/recipient address - * @param routerAddress Router contract address (receives ETH from swap) - * @param inputAmount USDC input amount - * @param feeAmount Post-swap fee in wei (ETH) - * @param ooRouterAddress OpenOcean router address - * @param swapData OpenOcean swap calldata - * @param stargateData Pre-built Stargate send() calldata + * amountLD (from stargateData) = minAmountOut - fee - nativeFeeWithBuffer. + * StargatePoolNative check: msg.value >= amountLD + nativeFee; + * bridgeValue = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee ✓ + * Any ETH surplus over minAmountOut stays in the router as unspent value. */ -function buildModularActions( - signerAddress: string, +function buildNativePoolModularActions( + signer: string, routerAddress: string, + cfg: CaseConfig, inputAmount: bigint, feeAmount: bigint, - bridgeValue: bigint, - ooRouterAddress: string, + nativeFeeWithBuffer: bigint, + minAmountOut: bigint, + ooRouter: string, swapData: string, stargateData: string, ): ModularAction[] { const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.USDC_ARB, - signerAddress, - routerAddress, - inputAmount, - ]); - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.USDC_ARB, encodeApprove(ooRouterAddress, inputAmount)); - exec.call(ooRouterAddress, swapData); - exec.nativeCall(signerAddress, '0x', feeAmount); - exec.nativeCall(STARGATE_NATIVE_ARB, stargateData, bridgeValue); + + exec.call( + ALLOWANCE_HOLDER, + ahIface.encodeFunctionData('transferFrom', [cfg.inputToken, signer, routerAddress, inputAmount]), + ); + exec.call(cfg.inputToken, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); // USDC → native ETH lands in router + exec.nativeCall(signer, '0x', feeAmount); // post-swap fee in ETH + // Bridge: value = amountLD + nativeFeeWithBuffer = minAmountOut - feeAmount + const bridgeValue = minAmountOut - feeAmount; + exec.nativeCall(cfg.stargatePool, stargateData, bridgeValue); + return exec.toActions(); } -// ─── Main ───────────────────────────────────────────────────────────────────── +/** + * Modular for ERC20-pool case (case 2): + * [0] AH.transferFrom USDC + * [1] USDC.transfer(signer, fee) + * [2] USDC.approve(stargatePool, MaxUint256) + * [3] STATICCALL USDC.balanceOf(router) — return value spliced into [4] + * [4] nativeCall: Stargate send() with nativeFeeWithBuffer POL; + * splicePayloadWord(STARGATE_AMOUNT_LD_OFFSET): CALL_WITH_NATIVE data is + * [32-byte native value prefix][ethers send calldata]; amountLD stays at +196 + * within the payload slice (matches OpenOceanStargateNativeOpenRouterPoC.t.sol). + */ +function buildErc20PoolModularActions( + signer: string, + routerAddress: string, + cfg: CaseConfig, + inputAmount: bigint, + feeAmount: bigint, + nativeFeeWithBuffer: bigint, + stargateData: string, +): ModularAction[] { + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); -async function main() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } + exec.call( + ALLOWANCE_HOLDER, + ahIface.encodeFunctionData('transferFrom', [cfg.inputToken, signer, routerAddress, inputAmount]), + ); + exec.call(cfg.inputToken, encodeTransfer(signer, feeAmount)); // USDC fee to signer + exec.call(cfg.inputToken, encodeApprove(cfg.stargatePool, ethers.MaxUint256)); + const usdcBalance = exec.staticCall(cfg.inputToken, encodeBalanceOf(routerAddress)); + exec + .nativeCall(cfg.stargatePool, stargateData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); - const useModular = false; - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); + return exec.toActions(); +} - // ── 1. Read full USDC balance ─────────────────────────────────────────────── - const inputToken = TOKENS.USDC_ARB; - const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (inputAmount === 0n) { - throw new Error( - `Signer ${signerAddress} has zero USDC balance on Arbitrum. ` + - 'Fund the wallet with USDC on Arbitrum first.', - ); +// ─── Execution leg ──────────────────────────────────────────────────────────── + +/** + * Runs one monolithic or modular leg for a case. + * Fetches quotes, builds calldata, ensures AH allowance, and executes. + */ +async function executeLeg( + legLabel: string, + useModular: boolean, + cfg: CaseConfig, + routerAddress: string, + signer: ethers.Wallet, + signerAddress: string, + provider: ethers.JsonRpcProvider, + inputAmount: bigint, + routerIface: ethers.Interface, +): Promise { + console.log(`\n── ${legLabel} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); + + let feeAmount: bigint; + let minAmountOut = 0n; + let estimatedBridgeAmount: bigint; + let ooRouter = ''; + let swapData = ''; + + if (cfg.ooSwap !== null) { + // Cases 1 & 3: OO swap → native ETH + console.log(`Fetching OpenOcean quote (${cfg.ooSwap.inToken} → native ETH)...`); + const q = await fetchOoQuote(cfg.ooSwap, routerAddress, inputAmount); + ooRouter = q.ooRouter; + swapData = q.swapData; + feeAmount = bpsOf(q.estimatedOut, FEE_BPS); + estimatedBridgeAmount = q.estimatedOut - feeAmount; + minAmountOut = q.minAmountOut; + + console.log(` OO router: ${ooRouter}`); + console.log(` Est. out: ${ethers.formatEther(q.estimatedOut)} ETH`); + console.log(` Fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min out: ${ethers.formatEther(minAmountOut)} ETH`); + } else { + // Case 2: no OO swap — bridge entire balance minus fee + feeAmount = bpsOf(inputAmount, FEE_BPS); + estimatedBridgeAmount = inputAmount - feeAmount; + console.log(` Fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); } - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ADDRESS}`); - console.log(`Input token: ${inputToken} (USDC Arbitrum)`); - console.log(`Input: ${ethers.formatUnits(inputAmount, inputDecimals)} USDC (full wallet balance)`); - console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); - console.log(''); - - // ── 2. Fetch OpenOcean swap quote ────────────────────────────────────────── - console.log('Fetching OpenOcean swap quote (USDC → native ETH, Arbitrum)...'); - const { - routerAddress: ooRouterAddress, - swapData, - minAmountOut, - estimatedOut, - } = await fetchOpenOceanSwapQuote(ROUTER_ADDRESS, inputAmount); - - const feeAmount = bpsOf(estimatedOut, FEE_BPS); - const estimatedFinalAmount = estimatedOut - feeAmount; - - console.log(`OO Router: ${ooRouterAddress}`); - console.log(`Est. ETH out: ${ethers.formatEther(estimatedOut)} ETH`); - console.log(`Post-swap fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); - console.log(`Est. final amount: ${ethers.formatEther(estimatedFinalAmount)} ETH`); - console.log(`Min ETH out (OO): ${ethers.formatEther(minAmountOut)} ETH`); - console.log(''); - - // ── 3. Fetch Stargate quote (nativeFee + expected receive amount) ─────────── - console.log('Fetching Stargate quoteSend (ETH Arbitrum → ETH Base)...'); + // Fetch Stargate quote for nativeFee and expected receive amount + console.log(`Fetching Stargate quoteSend (pool ${cfg.stargatePool})...`); const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + cfg.stargatePool, provider, - signerAddress, // recipient on Base - estimatedFinalAmount, // tentative amount for quoting + cfg.destLzEid, + signerAddress, + estimatedBridgeAmount, ); - - // Add 5% buffer to nativeFee to guard against LZ fee fluctuations between - // quote time and tx inclusion (mirrors the EVM_NATIVE_FEE_BUFFER_PERCENT pattern - // in oft.service.ts). const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; - // amountLD to encode in the send() calldata. - // - // Stargate requires msg.value >= amountLD + nativeFee. With - // useFinalAmountAsValue=true, msg.value = finalAmount = actualSwapOut - feeAmount. - // - // To guarantee this holds even under maximum OO slippage we base amountLD on - // minAmountOut (OO's slippage floor) rather than estimatedOut: - // - // amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer - // - // Because feeAmount is a fixed pre-encoded value and feeAmount <= estimatedOut, - // we know actualSwapOut >= minAmountOut, so: - // finalAmount = actualSwapOut - feeAmount >= minAmountOut - feeAmount - // = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee ✓ - // - // Any excess ETH (finalAmount - amountLD - nativeFee) is refunded to the signer - // by Stargate via refundAddress. - // - // Using estimatedFinalAmount instead here will fail when slippage causes - // finalAmount < estimatedFinalAmount (Stargate_InvalidAmount 0x3442dd95). - const minFinalAmount = minAmountOut - feeAmount; - const amountLD = minFinalAmount - nativeFeeWithBuffer; - if (amountLD <= 0n) { - throw new Error( - `minAmountOut (${ethers.formatEther(minAmountOut)}) is too small to cover ` + - `feeAmount (${ethers.formatEther(feeAmount)}) + nativeFee (${ethers.formatEther(nativeFeeWithBuffer)}). ` + - 'Increase your USDC balance.', - ); + const nativeSymbol = cfg.sourceChainId === CHAIN_IDS.POLYGON ? 'POL' : 'ETH'; + console.log(` nativeFee: ${ethers.formatEther(nativeFee)} ${nativeSymbol}`); + console.log(` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} ${nativeSymbol}`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, cfg.isNativePool ? 18 : 6)}`); + + // Build Stargate calldata + let amountLD: bigint; + if (cfg.isNativePool) { + // Use minAmountOut as the basis so that msg.value (= actualFinalAmount) >= amountLD + nativeFeeWithBuffer + // is always satisfied: since actual >= min is guaranteed by OO slippage, + // actual - fee >= min - fee = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee ✓ + amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) { + throw new Error(`${cfg.name}: minAmountOut too small to cover fee + nativeFee.`); + } + } else { + amountLD = 0n; // placeholder — spliced by amountPositions or spliceWord at runtime } + const stargateData = buildStargateCalldata(cfg.destLzEid, nativeFeeWithBuffer, signerAddress, amountLD); - console.log(`Stargate nativeFee: ${ethers.formatEther(nativeFee)} ETH`); - console.log(`nativeFee (+5% buf): ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); - console.log(`amountLD (encoded): ${ethers.formatEther(amountLD)} ETH ← based on minAmountOut; excess refunded on-chain`); - console.log(`Est. received Base: ${ethers.formatEther(amountReceivedLD)} ETH`); - console.log(''); - - // ── 4. Build Stargate send() calldata ────────────────────────────────────── - const stargateData = buildStargateCalldata(amountLD, nativeFeeWithBuffer, signerAddress); - - // ── 5. Build router execution calldata ───────────────────────────────────── - const routerIface = new ethers.Interface(ROUTER_ABI); + // Build execution calldata let execCalldata: string; - if (useModular) { - const actions = buildModularActions( - signerAddress, - ROUTER_ADDRESS, - inputAmount, - feeAmount, - amountLD + nativeFeeWithBuffer, - ooRouterAddress, - swapData, - stargateData, - ); + const actions = cfg.isNativePool + ? buildNativePoolModularActions( + signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, + minAmountOut, ooRouter, swapData, stargateData, + ) + : buildErc20PoolModularActions( + signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, stargateData, + ); execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); - console.log('Using performModularExecution (modular)'); } else { - const exec = buildMonolithicExecution( - signerAddress, - inputAmount, - feeAmount, - minAmountOut, - ooRouterAddress, - swapData, - stargateData, - ); - execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); - console.log('Using performExecution (monolithic)'); + const mono = cfg.isNativePool + ? buildNativePoolMonolithic( + signerAddress, cfg, inputAmount, feeAmount, minAmountOut, + ooRouter, swapData, stargateData, + ) + : buildErc20PoolMonolithic( + signerAddress, cfg, inputAmount, feeAmount, stargateData, nativeFeeWithBuffer, + ); + execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); } - // ── 6. Ensure AllowanceHolder ERC20 approval ─────────────────────────────── - // USDC must be approved to AllowanceHolder before the exec call. - // Native ETH (the bridge token) does not require ERC20 approval. - await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - - // ── 7. Execute via AllowanceHolder.exec ──────────────────────────────────── - // msg.value = nativeFeeWithBuffer (forwarded to the router alongside USDC pull). - // The router needs this ETH in its balance so that after the swap: - // router.balance = actualFinalAmount + nativeFeeWithBuffer - // Stargate action msg.value = prequoted amountLD + nativeFeeWithBuffer - console.log( - `Sending AllowanceHolder.exec with msg.value = ${ethers.formatEther(nativeFeeWithBuffer)} ETH (LZ fee)...`, - ); + // Ensure AH allowance for the input ERC20 + await ensureAllowanceForAllowanceHolder(signer, cfg.inputToken, inputAmount); + + // txValue: + // Native pool: nativeFeeWithBuffer forwarded to give router enough ETH headroom + // ERC20 pool: nativeFeeWithBuffer forwarded so router can pay the LZ fee + const txValue = nativeFeeWithBuffer; + console.log(`AllowanceHolder.exec (txValue = ${ethers.formatEther(txValue)} ${nativeSymbol})...`); + const receipt = await execViaAH( signer, - ROUTER_ADDRESS, // operator — the contract allowed to pull USDC via AH.transferFrom - TOKENS.USDC_ARB, // token AH is granting ephemeral allowance for - inputAmount, // amount of USDC allowed - ROUTER_ADDRESS, // target — the router to call with execCalldata - execCalldata, // encoded performExecution / performModularExecution call - nativeFeeWithBuffer, // msg.value forwarded through AH to cover the LZ nativeFee + routerAddress, + cfg.inputToken, + inputAmount, + routerAddress, + execCalldata, + txValue, ); - console.log(''); - console.log(`Transaction hash: ${receipt.hash}`); - console.log(`Gas used: ${receipt.gasUsed?.toString() ?? 'unknown'}`); - console.log(''); - console.log('Swap and bridge submitted successfully.'); - console.log(` Swapped: USDC Arbitrum → ETH Arbitrum (~${ethers.formatEther(estimatedOut)} ETH)`); - console.log(` Fee: ~${ethers.formatEther(feeAmount)} ETH to ${signerAddress}`); - console.log(` Bridging: ~${ethers.formatEther(amountLD)} ETH → ETH Base via Stargate`); - console.log(` Recipient on Base: ${signerAddress}`); + logTxnSummary( + `${cfg.name} — ${useModular ? 'Modular' : 'Monolithic'}`, + cfg.sourceChainId, + receipt, + ); +} + +// ─── Run one case (monolithic + sleep + modular) ────────────────────────────── + +async function runCase( + cfg: CaseConfig, + signer: ethers.Wallet, + signerAddress: string, + routerIface: ethers.Interface, +): Promise { + const routerAddress = routerAddressForChain(cfg.sourceChainId); + console.log(`\n${'═'.repeat(70)}`); + console.log(`CASE: ${cfg.name}`); + console.log('═'.repeat(70)); + console.log(`Router (chain ${cfg.sourceChainId}): ${routerAddress}`); + + const provider = new ethers.JsonRpcProvider(cfg.rpc); + const signerOnChain = signer.connect(provider); + + const { balance: walletBalance, decimals } = await getWalletErc20Balance( + cfg.inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `${cfg.name}: signer ${signerAddress} has zero balance of ${cfg.inputToken} on chain ${cfg.sourceChainId}.`, + ); + } + + // const legAmount = walletBalance / 2n; + const legAmount = walletBalance; + if (legAmount === 0n) { + throw new Error(`${cfg.name}: balance too small to split into two halves.`); + } + + console.log(`Input token balance: ${ethers.formatUnits(walletBalance, decimals)} (${cfg.inputToken})`); + console.log(`Per leg: ${ethers.formatUnits(legAmount, decimals)}`); + + // await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface); + + console.log('\nSleeping 3s before modular leg...'); + await sleep(3000); + + await executeLeg('2/2', true, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface); +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const cfg = resolveScenarioConfig(); + + // Use any provider to create the wallet; the case reconnects via `runCase`. + const signer = new ethers.Wallet(privateKey); + const signerAddress = await signer.getAddress(); + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${routerAddressForChain(cfg.sourceChainId)} (chain ${cfg.sourceChainId})`); + console.log(`Scenario: ${process.argv[2] ?? process.env.STARGATE_E2E_CASE ?? '(resolved)'}`); + + await runCase(cfg, signer, signerAddress, routerIface); + + console.log('\n✓ Stargate case completed.'); } main().catch((err) => { diff --git a/scripts/e2e/utils/relayLinkQuote.ts b/scripts/e2e/utils/relayLinkQuote.ts new file mode 100644 index 0000000..39f3ff7 --- /dev/null +++ b/scripts/e2e/utils/relayLinkQuote.ts @@ -0,0 +1,88 @@ +/** + * Shared Relay.link quote/v2 fetch + approve/deposit parsing (used by e2e scripts). + */ +import axios from 'axios'; +import { ethers } from 'ethers'; + +import { RELAY_API_KEY } from '../config'; + +export interface RelayQuoteResponse { + steps: RelayStep[]; +} + +interface RelayStep { + items: Array<{ + data: { + to?: string; + data?: string; + }; + }>; +} + +export async function fetchRelayQuoteV2(params: { + routerAddress: string; + recipient: string; + originChainId: number; + destinationChainId: number; + originCurrency: string; + destinationCurrency: string; + amount: bigint; +}): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (RELAY_API_KEY) { + headers['x-api-key'] = RELAY_API_KEY; + } + + const body = { + user: params.routerAddress, + recipient: params.recipient, + originChainId: params.originChainId, + destinationChainId: params.destinationChainId, + originCurrency: params.originCurrency, + destinationCurrency: params.destinationCurrency, + tradeType: 'EXACT_INPUT', + amount: params.amount.toString(), + }; + + const response = await axios.post( + 'https://api.relay.link/quote/v2', + body, + { headers }, + ); + return response.data; +} + +export function parseRelayQuote(quote: RelayQuoteResponse): { + relaySpender: string; + depositTarget: string; + depositData: string; +} { + const approveIface = new ethers.Interface([ + 'function approve(address spender, uint256 amount) external returns (bool)', + ]); + + const approveStep = quote.steps[0]; + const approveDataHex = approveStep.items[0].data.data ?? ''; + let relaySpender: string; + try { + relaySpender = ethers.getAddress( + approveIface.decodeFunctionData('approve', approveDataHex)[0], + ); + } catch { + const normalized = approveDataHex.startsWith('0x') ? approveDataHex.slice(2) : approveDataHex; + if (normalized.length < 8 + 64) { + throw new Error('Relay approve step calldata too short for fallback spender parse'); + } + const spender40 = normalized.slice(8 + 24, 8 + 24 + 40); + relaySpender = ethers.getAddress('0x' + spender40); + } + + const depositStep = quote.steps[1]; + const depositItem = depositStep.items[0].data; + const depositTarget = depositItem.to ?? ''; + const depositData = depositItem.data ?? '0x'; + + return { relaySpender, depositTarget, depositData }; +} diff --git a/scripts/e2e/utils/sleep.ts b/scripts/e2e/utils/sleep.ts new file mode 100644 index 0000000..ae67cc7 --- /dev/null +++ b/scripts/e2e/utils/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/scripts/e2e/utils/txnLogSummary.ts b/scripts/e2e/utils/txnLogSummary.ts new file mode 100644 index 0000000..7f1fa2d --- /dev/null +++ b/scripts/e2e/utils/txnLogSummary.ts @@ -0,0 +1,24 @@ +import type { TransactionReceipt } from 'ethers'; + +import { BLOCK_EXPLORER_TX_PREFIX } from '../config'; + +export function explorerTxUrl(chainId: number, txHash: string): string { + const prefix = BLOCK_EXPLORER_TX_PREFIX[chainId]; + + if (!prefix) { + return txHash; + } + + return `${prefix}${txHash}`; +} + +export function logTxnSummary( + headline: string, + chainId: number, + receipt: TransactionReceipt, +): void { + console.log(''); + console.log(headline); + console.log(explorerTxUrl(chainId, receipt.hash)); + console.log(`Gas: ${receipt.gasUsed.toLocaleString('en-US')}`); +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..7ffd61c --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2139 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + +"@arbitrum/sdk@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-4.0.5.tgz#c7abf6fcec72b36faee7af704245a7e14c49ad7f" + integrity sha512-bADi4kVzSBUAV+GkxuKMx7zrkCVahIE4+fkBi0Ee18EPqGt1Wiub+yQCGTh+llApn1RpRtwwtYeZXhz9XelqGQ== + dependencies: + "@ethersproject/address" "^5.0.8" + "@ethersproject/bignumber" "^5.1.1" + "@ethersproject/bytes" "^5.0.8" + async-mutex "^0.4.0" + ethers "^5.1.0" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@ethereumjs/rlp@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842" + integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA== + +"@ethereumjs/util@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-9.1.0.tgz#75e3898a3116d21c135fa9e29886565609129bce" + integrity sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog== + dependencies: + "@ethereumjs/rlp" "^5.0.2" + ethereum-cryptography "^2.2.1" + +"@ethersproject/abi@5.8.0", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.8.0.tgz#e79bb51940ac35fe6f3262d7fe2cdb25ad5f07d9" + integrity sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q== + dependencies: + "@ethersproject/address" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/constants" "^5.8.0" + "@ethersproject/hash" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + +"@ethersproject/abstract-provider@5.8.0", "@ethersproject/abstract-provider@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz#7581f9be601afa1d02b95d26b9d9840926a35b0c" + integrity sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg== + dependencies: + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/networks" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/transactions" "^5.8.0" + "@ethersproject/web" "^5.8.0" + +"@ethersproject/abstract-signer@5.8.0", "@ethersproject/abstract-signer@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz#8d7417e95e4094c1797a9762e6789c7356db0754" + integrity sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA== + dependencies: + "@ethersproject/abstract-provider" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + +"@ethersproject/address@5.8.0", "@ethersproject/address@^5.0.8", "@ethersproject/address@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.8.0.tgz#3007a2c352eee566ad745dca1dbbebdb50a6a983" + integrity sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA== + dependencies: + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/rlp" "^5.8.0" + +"@ethersproject/base64@5.8.0", "@ethersproject/base64@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.8.0.tgz#61c669c648f6e6aad002c228465d52ac93ee83eb" + integrity sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ== + dependencies: + "@ethersproject/bytes" "^5.8.0" + +"@ethersproject/basex@5.8.0", "@ethersproject/basex@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.8.0.tgz#1d279a90c4be84d1c1139114a1f844869e57d03a" + integrity sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + +"@ethersproject/bignumber@5.8.0", "@ethersproject/bignumber@^5.1.1", "@ethersproject/bignumber@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.8.0.tgz#c381d178f9eeb370923d389284efa19f69efa5d7" + integrity sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.8.0", "@ethersproject/bytes@^5.0.8", "@ethersproject/bytes@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.8.0.tgz#9074820e1cac7507a34372cadeb035461463be34" + integrity sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A== + dependencies: + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/constants@5.8.0", "@ethersproject/constants@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.8.0.tgz#12f31c2f4317b113a4c19de94e50933648c90704" + integrity sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg== + dependencies: + "@ethersproject/bignumber" "^5.8.0" + +"@ethersproject/contracts@5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.8.0.tgz#243a38a2e4aa3e757215ea64e276f8a8c9d8ed73" + integrity sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ== + dependencies: + "@ethersproject/abi" "^5.8.0" + "@ethersproject/abstract-provider" "^5.8.0" + "@ethersproject/abstract-signer" "^5.8.0" + "@ethersproject/address" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/constants" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/transactions" "^5.8.0" + +"@ethersproject/hash@5.8.0", "@ethersproject/hash@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.8.0.tgz#b8893d4629b7f8462a90102572f8cd65a0192b4c" + integrity sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA== + dependencies: + "@ethersproject/abstract-signer" "^5.8.0" + "@ethersproject/address" "^5.8.0" + "@ethersproject/base64" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + +"@ethersproject/hdnode@5.8.0", "@ethersproject/hdnode@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.8.0.tgz#a51ae2a50bcd48ef6fd108c64cbae5e6ff34a761" + integrity sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA== + dependencies: + "@ethersproject/abstract-signer" "^5.8.0" + "@ethersproject/basex" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/pbkdf2" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/sha2" "^5.8.0" + "@ethersproject/signing-key" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + "@ethersproject/transactions" "^5.8.0" + "@ethersproject/wordlists" "^5.8.0" + +"@ethersproject/json-wallets@5.8.0", "@ethersproject/json-wallets@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz#d18de0a4cf0f185f232eb3c17d5e0744d97eb8c9" + integrity sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w== + dependencies: + "@ethersproject/abstract-signer" "^5.8.0" + "@ethersproject/address" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/hdnode" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/pbkdf2" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/random" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + "@ethersproject/transactions" "^5.8.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/keccak256@5.8.0", "@ethersproject/keccak256@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.8.0.tgz#d2123a379567faf2d75d2aaea074ffd4df349e6a" + integrity sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng== + dependencies: + "@ethersproject/bytes" "^5.8.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@5.8.0", "@ethersproject/logger@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.8.0.tgz#f0232968a4f87d29623a0481690a2732662713d6" + integrity sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA== + +"@ethersproject/networks@5.8.0", "@ethersproject/networks@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.8.0.tgz#8b4517a3139380cba9fb00b63ffad0a979671fde" + integrity sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg== + dependencies: + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/pbkdf2@5.8.0", "@ethersproject/pbkdf2@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz#cd2621130e5dd51f6a0172e63a6e4a0c0a0ec37e" + integrity sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/sha2" "^5.8.0" + +"@ethersproject/properties@5.8.0", "@ethersproject/properties@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.8.0.tgz#405a8affb6311a49a91dabd96aeeae24f477020e" + integrity sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw== + dependencies: + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/providers@5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.8.0.tgz#6c2ae354f7f96ee150439f7de06236928bc04cb4" + integrity sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw== + dependencies: + "@ethersproject/abstract-provider" "^5.8.0" + "@ethersproject/abstract-signer" "^5.8.0" + "@ethersproject/address" "^5.8.0" + "@ethersproject/base64" "^5.8.0" + "@ethersproject/basex" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/constants" "^5.8.0" + "@ethersproject/hash" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/networks" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/random" "^5.8.0" + "@ethersproject/rlp" "^5.8.0" + "@ethersproject/sha2" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + "@ethersproject/transactions" "^5.8.0" + "@ethersproject/web" "^5.8.0" + bech32 "1.1.4" + ws "8.18.0" + +"@ethersproject/random@5.8.0", "@ethersproject/random@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.8.0.tgz#1bced04d49449f37c6437c701735a1a022f0057a" + integrity sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/rlp@5.8.0", "@ethersproject/rlp@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.8.0.tgz#5a0d49f61bc53e051532a5179472779141451de5" + integrity sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/sha2@5.8.0", "@ethersproject/sha2@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.8.0.tgz#8954a613bb78dac9b46829c0a95de561ef74e5e1" + integrity sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@5.8.0", "@ethersproject/signing-key@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.8.0.tgz#9797e02c717b68239c6349394ea85febf8893119" + integrity sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + bn.js "^5.2.1" + elliptic "6.6.1" + hash.js "1.1.7" + +"@ethersproject/solidity@5.8.0", "@ethersproject/solidity@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.8.0.tgz#429bb9fcf5521307a9448d7358c26b93695379b9" + integrity sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA== + dependencies: + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/sha2" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + +"@ethersproject/strings@5.8.0", "@ethersproject/strings@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.8.0.tgz#ad79fafbf0bd272d9765603215ac74fd7953908f" + integrity sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/constants" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/transactions@5.8.0", "@ethersproject/transactions@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.8.0.tgz#1e518822403abc99def5a043d1c6f6fe0007e46b" + integrity sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg== + dependencies: + "@ethersproject/address" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/constants" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/rlp" "^5.8.0" + "@ethersproject/signing-key" "^5.8.0" + +"@ethersproject/units@5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.8.0.tgz#c12f34ba7c3a2de0e9fa0ed0ee32f3e46c5c2c6a" + integrity sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ== + dependencies: + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/constants" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + +"@ethersproject/wallet@5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.8.0.tgz#49c300d10872e6986d953e8310dc33d440da8127" + integrity sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA== + dependencies: + "@ethersproject/abstract-provider" "^5.8.0" + "@ethersproject/abstract-signer" "^5.8.0" + "@ethersproject/address" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/hash" "^5.8.0" + "@ethersproject/hdnode" "^5.8.0" + "@ethersproject/json-wallets" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/random" "^5.8.0" + "@ethersproject/signing-key" "^5.8.0" + "@ethersproject/transactions" "^5.8.0" + "@ethersproject/wordlists" "^5.8.0" + +"@ethersproject/web@5.8.0", "@ethersproject/web@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.8.0.tgz#3e54badc0013b7a801463a7008a87988efce8a37" + integrity sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw== + dependencies: + "@ethersproject/base64" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + +"@ethersproject/wordlists@5.8.0", "@ethersproject/wordlists@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.8.0.tgz#7a5654ee8d1bb1f4dbe43f91d217356d650ad821" + integrity sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg== + dependencies: + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/hash" "^5.8.0" + "@ethersproject/logger" "^5.8.0" + "@ethersproject/properties" "^5.8.0" + "@ethersproject/strings" "^5.8.0" + +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@layerzerolabs/lz-v2-utilities@^3.0.168": + version "3.0.168" + resolved "https://registry.yarnpkg.com/@layerzerolabs/lz-v2-utilities/-/lz-v2-utilities-3.0.168.tgz#a7b62a1422f978151ca0f3fc77e3ab511f9e6eb3" + integrity sha512-5gb5QH3q+JIOkwuJnmv3hWidwLE7ySC0G4IYCL9pwl80bkdkuY9TwfG3KqWri2F5mCzRYs6xx7vaYP5zFqY7yA== + dependencies: + "@ethersproject/abi" "^5.8.0" + "@ethersproject/address" "^5.8.0" + "@ethersproject/bignumber" "^5.8.0" + "@ethersproject/bytes" "^5.8.0" + "@ethersproject/keccak256" "^5.8.0" + "@ethersproject/solidity" "^5.8.0" + bs58 "^5.0.0" + tiny-invariant "^1.3.1" + +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/curves@~1.8.1": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.2.tgz#8f24c037795e22b90ae29e222a856294c1d9ffc7" + integrity sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g== + dependencies: + "@noble/hashes" "1.7.2" + +"@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" + integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@1.4.0", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/hashes@1.7.2", "@noble/hashes@~1.7.1": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" + integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== + +"@noble/secp256k1@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" + integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== + +"@noble/secp256k1@~1.7.0": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.2.tgz#c2c3343e2dce80e15a914d7442147507f8a98e7f" + integrity sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ== + +"@nomicfoundation/edr-darwin-arm64@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.23.tgz#b1587edd46476d271b3dd54c024054a964fa9f66" + integrity sha512-Amh7mRoDzZyJJ4efqoePqdoZOzharmSOttZuJDlVE5yy07BoE8hL6ZRpa5fNYn0LCqn/KoWs8OHANWxhKDGhvQ== + +"@nomicfoundation/edr-darwin-x64@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.23.tgz#6b61903e93eb40716ea3ed7c4f24f793e6566591" + integrity sha512-9wn489FIQm7m0UCD+HhktjWx6vskZzeZD9oDc2k9ZvbBzdXwPp5tiDqUBJ+eQpByAzCDfteAJwRn2lQCE0U+Iw== + +"@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.23.tgz#0c141a621c9dbe6b0dc414da5913b1f94833676e" + integrity sha512-nlk5EejSzEUfEngv0Jkhqq3/wINIfF2ED9wAofc22w/V1DV99ASh9l3/e/MIHOQFecIZ9MDqt0Em9/oDyB1Uew== + +"@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.23.tgz#bd996171aa0f90eb722984436449fd2ddb4de065" + integrity sha512-SJuPBp3Rc6vM92UtVTUxZQ/QlLhLfwTftt2XUiYohmGKB3RjGzpgduEFMCA0LEnucUckU6UHrJNFHiDm77C4PQ== + +"@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.23.tgz#fb411c5b5efeb96d0d859bb34ff0f466201c3f5d" + integrity sha512-NU+Qs3u7Qt6t3bJFdmmjd5CsvgI2bPPzO31KifM2Ez96/jsXYho5debtTQnimlb5NAqiHTSlxjh/F8ROcptmeQ== + +"@nomicfoundation/edr-linux-x64-musl@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.23.tgz#18685a6489167d1386db8d31823ce7d92fd789b5" + integrity sha512-F78fZA2h6/ssiCSZOovlgIu0dUeI7ItKPsDDF3UUlIibef052GCXmliMinC90jVPbrjUADMd1BUwjfI0Z8OllQ== + +"@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.23.tgz#20cc6fcdb22500df1e48fab0a397bc30a22ec37f" + integrity sha512-IfJZQJn7d/YyqhmguBIGoCKjE9dKjbu6V6iNEPApfwf5JyyjHYyyfkLU4rf7hygj57bfH4sl1jtQ6r8HnT62lw== + +"@nomicfoundation/edr@0.12.0-next.23": + version "0.12.0-next.23" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.12.0-next.23.tgz#483b29bed5165bf6f97b9be01f1536d0f1e1845a" + integrity sha512-F2/6HZh8Q9RsgkOIkRrckldbhPjIZY7d4mT9LYuW68miwGQ5l7CkAgcz9fRRiurA0+YJhtsbx/EyrD9DmX9BOw== + dependencies: + "@nomicfoundation/edr-darwin-arm64" "0.12.0-next.23" + "@nomicfoundation/edr-darwin-x64" "0.12.0-next.23" + "@nomicfoundation/edr-linux-arm64-gnu" "0.12.0-next.23" + "@nomicfoundation/edr-linux-arm64-musl" "0.12.0-next.23" + "@nomicfoundation/edr-linux-x64-gnu" "0.12.0-next.23" + "@nomicfoundation/edr-linux-x64-musl" "0.12.0-next.23" + "@nomicfoundation/edr-win32-x64-msvc" "0.12.0-next.23" + +"@nomicfoundation/hardhat-foundry@^1.1.2": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.2.1.tgz#2d0b8bd8d2815a4217d05381520520a6b74c2d3c" + integrity sha512-pH1KeyI0sysgi7I7uQKPLXWl895EkuS6V41rSi820Ipqp/FScIwDh27RbevgC9zJ4ufSsSz34njm9cvRMGMNVA== + dependencies: + picocolors "^1.1.0" + +"@nomicfoundation/hardhat-toolbox@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-5.0.0.tgz#165b47f8a3d2bf668cc5d453ce7f496a1156948d" + integrity sha512-FnUtUC5PsakCbwiVNsqlXVIWG5JIb5CEZoSXbJUsEBun22Bivx2jhF1/q9iQbzuaGpJKFQyOhemPB2+XlEE6pQ== + +"@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz#3a9c3b20d51360b20affb8f753e756d553d49557" + integrity sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw== + +"@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz#74dcfabeb4ca373d95bd0d13692f44fcef133c28" + integrity sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw== + +"@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz#4af5849a89e5a8f511acc04f28eb5d4460ba2b6a" + integrity sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA== + +"@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz#54036808a9a327b2ff84446c130a6687ee702a8e" + integrity sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA== + +"@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz#466cda0d6e43691986c944b909fc6dbb8cfc594e" + integrity sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g== + +"@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz#2b35826987a6e94444140ac92310baa088ee7f94" + integrity sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg== + +"@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz#e6363d13b8709ca66f330562337dbc01ce8bbbd9" + integrity sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA== + +"@nomicfoundation/solidity-analyzer@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz#8bcea7d300157bf3a770a851d9f5c5e2db34ac55" + integrity sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA== + optionalDependencies: + "@nomicfoundation/solidity-analyzer-darwin-arm64" "0.1.2" + "@nomicfoundation/solidity-analyzer-darwin-x64" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-arm64-musl" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-x64-gnu" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2" + "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2" + +"@scure/base@~1.1.0", "@scure/base@~1.1.6": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + +"@scure/base@~1.2.5": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" + integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== + +"@scure/bip32@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" + integrity sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw== + dependencies: + "@noble/hashes" "~1.2.0" + "@noble/secp256k1" "~1.7.0" + "@scure/base" "~1.1.0" + +"@scure/bip32@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" + integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + +"@scure/bip39@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" + integrity sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg== + dependencies: + "@noble/hashes" "~1.2.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" + integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== + dependencies: + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + +"@sentry/core@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" + integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/hub@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" + integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== + dependencies: + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/minimal@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" + integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/types" "5.30.0" + tslib "^1.9.3" + +"@sentry/node@^5.18.1": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" + integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== + dependencies: + "@sentry/core" "5.30.0" + "@sentry/hub" "5.30.0" + "@sentry/tracing" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/tracing@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" + integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/types@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" + integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== + +"@sentry/utils@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" + integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== + dependencies: + "@sentry/types" "5.30.0" + tslib "^1.9.3" + +"@tsconfig/node10@^1.0.7": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" + integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/node@22.7.5": + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + dependencies: + undici-types "~6.19.2" + +acorn-walk@^8.1.1: + version "8.3.5" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" + integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +adm-zip@^0.4.16: + version "0.4.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" + integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== + +aes-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== + +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@^4.1.1, ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +async-mutex@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.1.tgz#bccf55b96f2baf8df90ed798cb5544a1f6ee4c2c" + integrity sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA== + dependencies: + tslib "^2.4.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.16.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12" + integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A== + dependencies: + follow-redirects "^1.16.0" + form-data "^4.0.5" + https-proxy-agent "^5.0.1" + proxy-from-env "^2.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.1.tgz#817fb7b57143c501f649805cb247617ad016a885" + integrity sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw== + +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bn.js@^4.11.9: + version "4.12.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.3.tgz#2cc2c679188eb35b006f2d0d4710bed8437a769e" + integrity sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g== + +bn.js@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" + integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== + +boxen@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + +brace-expansion@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.0.tgz#4f41a41190216ee36067ec381526fe9539c4f0ae" + integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +bs58@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" + integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== + dependencies: + base-x "^4.0.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +camelcase@^6.0.0, camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +command-exists@^1.2.8: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + +commander@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +debug@4, debug@^4.1.1, debug@^4.3.5: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +diff@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== + +diff@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.2.tgz#0a4742797281d09cfa699b79ea32d27723623bad" + integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== + +dotenv@^16.0.0: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +elliptic@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" + integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enquirer@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== + dependencies: + ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +ethereum-cryptography@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz#5ccfa183e85fdaf9f9b299a79430c044268c9b3a" + integrity sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw== + dependencies: + "@noble/hashes" "1.2.0" + "@noble/secp256k1" "1.7.1" + "@scure/bip32" "1.1.5" + "@scure/bip39" "1.1.1" + +ethereum-cryptography@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz#58f2810f8e020aecb97de8c8c76147600b0b8ccf" + integrity sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg== + dependencies: + "@noble/curves" "1.4.2" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + +ethers@^5.1.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.8.0.tgz#97858dc4d4c74afce83ea7562fe9493cedb4d377" + integrity sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg== + dependencies: + "@ethersproject/abi" "5.8.0" + "@ethersproject/abstract-provider" "5.8.0" + "@ethersproject/abstract-signer" "5.8.0" + "@ethersproject/address" "5.8.0" + "@ethersproject/base64" "5.8.0" + "@ethersproject/basex" "5.8.0" + "@ethersproject/bignumber" "5.8.0" + "@ethersproject/bytes" "5.8.0" + "@ethersproject/constants" "5.8.0" + "@ethersproject/contracts" "5.8.0" + "@ethersproject/hash" "5.8.0" + "@ethersproject/hdnode" "5.8.0" + "@ethersproject/json-wallets" "5.8.0" + "@ethersproject/keccak256" "5.8.0" + "@ethersproject/logger" "5.8.0" + "@ethersproject/networks" "5.8.0" + "@ethersproject/pbkdf2" "5.8.0" + "@ethersproject/properties" "5.8.0" + "@ethersproject/providers" "5.8.0" + "@ethersproject/random" "5.8.0" + "@ethersproject/rlp" "5.8.0" + "@ethersproject/sha2" "5.8.0" + "@ethersproject/signing-key" "5.8.0" + "@ethersproject/solidity" "5.8.0" + "@ethersproject/strings" "5.8.0" + "@ethersproject/transactions" "5.8.0" + "@ethersproject/units" "5.8.0" + "@ethersproject/wallet" "5.8.0" + "@ethersproject/web" "5.8.0" + "@ethersproject/wordlists" "5.8.0" + +ethers@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.16.0.tgz#fff9b4f05d7a359c774ad6e91085a800f7fccf65" + integrity sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "22.7.5" + aes-js "4.0.0-beta.5" + tslib "2.7.0" + ws "8.17.1" + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.12.1, follow-redirects@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== + +form-data@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + +fp-ts@1.19.3: + version "1.19.3" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f" + integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg== + +fp-ts@^1.0.0: + version "1.19.5" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.5.tgz#3da865e585dfa1fdfd51785417357ac50afc520a" + integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== + +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +hardhat@^2.22.7: + version "2.28.6" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.28.6.tgz#1346f90796492097ee6a802e762a2f4883817db6" + integrity sha512-zQze7qe+8ltwHvhX5NQ8sN1N37WWZGw8L63y+2XcPxGwAjc/SMF829z3NS6o1krX0sryhAsVBK/xrwUqlsot4Q== + dependencies: + "@ethereumjs/util" "^9.1.0" + "@ethersproject/abi" "^5.1.2" + "@nomicfoundation/edr" "0.12.0-next.23" + "@nomicfoundation/solidity-analyzer" "^0.1.0" + "@sentry/node" "^5.18.1" + adm-zip "^0.4.16" + aggregate-error "^3.0.0" + ansi-escapes "^4.3.0" + boxen "^5.1.2" + chokidar "^4.0.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + ethereum-cryptography "^1.0.3" + find-up "^5.0.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + json-stream-stringify "^3.1.4" + keccak "^3.0.2" + lodash "^4.17.11" + micro-eth-signer "^0.14.0" + mnemonist "^0.38.0" + mocha "^10.0.0" + p-map "^4.0.0" + picocolors "^1.1.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + solc "0.8.26" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + tinyglobby "^0.2.6" + tsort "0.0.1" + undici "^5.14.0" + uuid "^8.3.2" + ws "^7.4.6" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasown@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c" + integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +iconv-lite@~0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +immutable@^4.0.0-rc.12: + version "4.3.8" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7" + integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +io-ts@1.10.4: + version "1.10.4" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2" + integrity sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g== + dependencies: + fp-ts "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +js-yaml@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +json-stream-stringify@^3.1.4: + version "3.1.6" + resolved "https://registry.yarnpkg.com/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz#ebe32193876fb99d4ec9f612389a8d8e2b5d54d4" + integrity sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +keccak@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" + integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== + dependencies: + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + readable-stream "^3.6.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.11: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + +micro-eth-signer@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz#8aa1fe997d98d6bdf42f2071cef7eb01a66ecb22" + integrity sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw== + dependencies: + "@noble/curves" "~1.8.1" + "@noble/hashes" "~1.7.1" + micro-packed "~0.7.2" + +micro-packed@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.7.3.tgz#59e96b139dffeda22705c7a041476f24cabb12b6" + integrity sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg== + dependencies: + "@scure/base" "~1.2.5" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + +mnemonist@^0.38.0: + version "0.38.5" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" + integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== + dependencies: + obliterator "^2.0.0" + +mocha@^10.0.0: + version "10.8.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" + integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +node-addon-api@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" + integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== + +node-gyp-build@^4.2.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +obliterator@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.5.tgz#031e0145354b0c18840336ae51d41e7d6d2c76aa" + integrity sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + +picomatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + +proxy-from-env@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" + integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +raw-body@^2.4.1: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + unpipe "~1.0.0" + +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve@1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +scrypt-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" + integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== + +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +solc@0.8.26: + version "0.8.26" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" + integrity sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g== + dependencies: + command-exists "^1.2.8" + commander "^8.1.0" + follow-redirects "^1.12.1" + js-sha3 "0.8.0" + memorystream "^0.3.1" + semver "^5.5.0" + tmp "0.0.33" + +source-map-support@^0.5.13: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +stacktrace-parser@^0.1.10: + version "0.1.11" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz#c7c08f9b29ef566b9a6f7b255d7db572f66fabc4" + integrity sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg== + dependencies: + type-fest "^0.7.1" + +statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +tiny-invariant@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tinyglobby@^0.2.6: + version "0.2.16" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" + integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tmp@0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-node@^10.9.0: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tsort@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" + integrity sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== + +typescript@^5.0.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +undici@^5.14.0: + version "5.29.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" + integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg== + dependencies: + "@fastify/busboy" "^2.0.0" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@^7.4.6: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From d66f476751ac153ceb5d7ba6e2db3d04d7c4b91e Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 14 May 2026 19:57:48 +0530 Subject: [PATCH 23/69] test: arb eth to base usdc stargate --- scripts/e2e/config.ts | 7 + scripts/e2e/swapBridgeViaStargateNative.ts | 244 +++++++++++++++++---- 2 files changed, 211 insertions(+), 40 deletions(-) diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index cb3ace2..164c6a6 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -173,6 +173,13 @@ export const STARGATE_NATIVE_ARB = '0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F'; */ export const STARGATE_NATIVE_BASE = '0xdc181Bd607330aeeBEF6ea62e03e5e1Fb4B6F7C7'; +/** + * Stargate v2 USDC pool on Arbitrum. + * ERC20 pool — caller approves USDC to this address; msg.value = nativeFee only. + * Source: https://stargateprotocol.gitbook.io/stargate/v2-developer-docs/technical-reference + */ +export const STARGATE_USDC_ARB = '0xe8CDF27AcD73a434D661C84887215F7598e7d0d3'; + /** * Stargate v2 USDC pool on Polygon PoS. * NOTE: Polygon has no StargatePoolNative for POL/MATIC — only USDC and USDT diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index 08671e9..cd471ed 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -5,6 +5,7 @@ * Case 1 Arbitrum USDC → OO swap → native ETH → Stargate Native ETH Pool → Base ETH * Case 2 Polygon USDC → (no swap) → Stargate USDC Pool → Base USDC * Case 3 Base USDC → OO swap → native ETH → Stargate Native ETH Pool → Arb ETH + * Case 4 Arbitrum ETH → OO swap → USDC Arb → Stargate USDC Pool → Base USDC * * Native-pool mechanics (cases 1 & 3): * send() requires msg.value >= amountLD + nativeFee (StargatePoolNative._assertMessagingFee). @@ -27,16 +28,18 @@ * 1 / arb-usdc-base-eth Arbitrum USDC → OO → native ETH → Stargate native → Base ETH * 2 / polygon-usdc-base Polygon USDC → Stargate USDC pool → Base USDC (no swap) * 3 / base-usdc-arb-eth Base USDC → OO → native ETH → Stargate native → Arbitrum ETH + * 4 / arb-eth-base-usdc Arbitrum ETH → OO → USDC → Stargate USDC pool → Base USDC + * msg.value = inputETH + nativeFee (native input + LZ fee) * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth - * STARGATE_E2E_CASE=2 PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts + * STARGATE_E2E_CASE=4 PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts * * Router per source chain: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(chainId)` in config.ts. * Override with `ROUTER_CHAIN_` env when needed. */ import axios from 'axios'; -import { ethers } from 'ethers'; +import { ethers, parseEther } from 'ethers'; import * as dotenv from 'dotenv'; dotenv.config(); @@ -53,6 +56,7 @@ import { STARGATE_NATIVE_ARB, STARGATE_NATIVE_BASE, STARGATE_USDC_POLYGON, + STARGATE_USDC_ARB, BASE_LZ_EID, ARBITRUM_LZ_EID, STARGATE_AMOUNT_LD_OFFSET, @@ -91,6 +95,8 @@ interface CaseConfig { rpc: string; inputToken: string; inputDecimals: number; + /** true when inputToken is native (ETH/POL); skips ERC20 AH allowance and adjusts txValue */ + isNativeInput: boolean; ooSwap: OoSwapConfig | null; // null → skip OO swap, bridge input token directly stargatePool: string; isNativePool: boolean; @@ -104,6 +110,7 @@ const CASES: CaseConfig[] = [ rpc: RPC.ARBITRUM, inputToken: TOKENS.USDC_ARB, inputDecimals: 6, + isNativeInput: false, ooSwap: { inToken: TOKENS.USDC_ARB, outToken: NATIVE_TOKEN_ADDRESS, @@ -121,6 +128,7 @@ const CASES: CaseConfig[] = [ rpc: RPC.POLYGON, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputDecimals: 6, + isNativeInput: false, ooSwap: null, // skip OO swap — bridge USDC directly stargatePool: STARGATE_USDC_POLYGON, isNativePool: false, @@ -132,6 +140,7 @@ const CASES: CaseConfig[] = [ rpc: RPC.BASE, inputToken: TOKENS.USDC_BASE, inputDecimals: 6, + isNativeInput: false, ooSwap: { inToken: TOKENS.USDC_BASE, outToken: NATIVE_TOKEN_ADDRESS, @@ -143,9 +152,30 @@ const CASES: CaseConfig[] = [ isNativePool: true, destLzEid: ARBITRUM_LZ_EID, }, + { + // msg.value = inputETH (swapped via OO) + nativeFeeWithBuffer (LZ fee). + // After the OO swap the router holds USDC + nativeFeeWithBuffer ETH, which it + // uses to pay the Stargate USDC pool's LZ fee. + name: 'Arbitrum ETH → USDC (OO) → Base USDC (Stargate USDC Pool)', + sourceChainId: CHAIN_IDS.ARBITRUM, + rpc: RPC.ARBITRUM, + inputToken: NATIVE_TOKEN_ADDRESS, + inputDecimals: 18, + isNativeInput: true, + ooSwap: { + inToken: NATIVE_TOKEN_ADDRESS, + outToken: TOKENS.USDC_ARB, + inDecimals: 18, + chainId: CHAIN_IDS.ARBITRUM, + gasPrice: '1', + }, + stargatePool: STARGATE_USDC_ARB, + isNativePool: false, + destLzEid: BASE_LZ_EID, + }, ]; -/** Slug aliases (and `1`/`2`/`3`) → index in `CASES`. */ +/** Slug aliases (and `1`/`2`/`3`/`4`) → index in `CASES`. */ const STARGATE_SCENARIO_ALIASES: Record = { '1': 0, 'arb-usdc-base-eth': 0, @@ -159,6 +189,10 @@ const STARGATE_SCENARIO_ALIASES: Record = { '3': 2, 'base-usdc-arb-eth': 2, 'base-native-arb': 2, + + '4': 3, + 'arb-eth-base-usdc': 3, + 'arb-native-usdc-base': 3, }; /** @@ -173,7 +207,8 @@ function resolveScenarioConfig(): CaseConfig { ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth\n' + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-usdc-base\n' + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts base-usdc-arb-eth\n' + - 'Or use numeric slugs 1 | 2 | 3.', + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc\n' + + 'Or use numeric slugs 1 | 2 | 3 | 4.', ); process.exit(1); } @@ -473,6 +508,99 @@ function buildErc20PoolModularActions( return exec.toActions(); } +/** + * ETH reserved from native balance for gas + LZ fee when inputToken is native. + * The balance read in runCase subtracts this before using the remainder as inputAmount, + * so the signer always has headroom to pay tx gas on top of (inputAmount + nativeFeeWithBuffer). + */ +const NATIVE_INPUT_GAS_RESERVE = parseEther("0.001"); + +// ─── Monolithic/modular builders for case 4 ─────────────────────────────────── + +/** + * Monolithic for case 4 (native ETH input → OO swap to USDC → Stargate USDC pool → Base USDC): + * - inputToken = NATIVE_TOKEN_ADDRESS; swap.approvalSpender = 0 (no ERC20 approve needed) + * - swap.value = inputAmount: forwards that ETH to OO which returns USDC to the router + * - postFee: router sends feeAmount USDC to signer + * - bridge.approvalSpender = stargatePool: router approves remaining USDC to Stargate + * - bridge.value = nativeFeeWithBuffer: only LZ fee in native (not the USDC bridge amount) + * - amountPositions=[196n]: router splices post-fee USDC finalAmount into stargateData.amountLD + * + * msg.value = inputAmount + nativeFeeWithBuffer: + * OO consumes inputAmount ETH → router holds USDC + nativeFeeWithBuffer ETH for the LZ fee. + */ +function buildNativeInErc20BridgeMonolithic( + signer: string, + cfg: CaseConfig, + inputAmount: bigint, + feeAmount: bigint, + minAmountOut: bigint, + ooRouter: string, + swapData: string, + stargateData: string, + nativeFeeWithBuffer: bigint, +): MonolithicExecution { + return { + input: { user: signer, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ZERO_ADDRESS, // no ERC20 approve for native ETH input + outputToken: cfg.ooSwap!.outToken, // USDC_ARB + value: inputAmount, // forward inputAmount ETH to OO + minOutput: minAmountOut, + data: swapData, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signer, amount: feeAmount }, // fee in USDC + bridge: { + target: cfg.stargatePool, + approvalSpender: cfg.stargatePool, // router approves USDC to Stargate pool + value: nativeFeeWithBuffer, // LZ fee only; not the USDC bridge amount + data: stargateData, + amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice USDC amountLD at runtime + useFinalAmountAsValue: false, + }, + }; +} + +/** + * Modular for case 4 (native ETH input → OO swap to USDC → Stargate USDC pool → Base USDC): + * [0] nativeCall(ooRouter, swapData, inputAmount) — send inputAmount ETH to OO, get USDC + * [1] USDC.transfer(signer, feeAmount) — fee out to signer + * [2] USDC.approve(stargatePool, MaxUint256) + * [3] STATICCALL USDC.balanceOf(router) → stored for splice into [4] + * [4] nativeCall(stargatePool, stargateData, nativeFeeWithBuffer) + * .splicePayloadWord(STARGATE_AMOUNT_LD_OFFSET) ← patches amountLD from [3] + * + * No AH.transferFrom step — input ETH is already in the router via msg.value forwarded by AH.exec. + * msg.value = inputAmount + nativeFeeWithBuffer (set by executeLeg for isNativeInput cases). + */ +function buildNativeInErc20BridgeModularActions( + signer: string, + routerAddress: string, + cfg: CaseConfig, + inputAmount: bigint, + feeAmount: bigint, + nativeFeeWithBuffer: bigint, + ooRouter: string, + swapData: string, + stargateData: string, +): ModularAction[] { + const exec = new ModularActionsBuilder(); + const usdcToken = cfg.ooSwap!.outToken; // USDC_ARB + + exec.nativeCall(ooRouter, swapData, inputAmount); // ETH → USDC, USDC lands in router + exec.call(usdcToken, encodeTransfer(signer, feeAmount)); + exec.call(usdcToken, encodeApprove(cfg.stargatePool, ethers.MaxUint256)); + const usdcBalance = exec.staticCall(usdcToken, encodeBalanceOf(routerAddress)); + exec + .nativeCall(cfg.stargatePool, stargateData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); + + return exec.toActions(); +} + // ─── Execution leg ──────────────────────────────────────────────────────────── /** @@ -499,8 +627,11 @@ async function executeLeg( let swapData = ''; if (cfg.ooSwap !== null) { - // Cases 1 & 3: OO swap → native ETH - console.log(`Fetching OpenOcean quote (${cfg.ooSwap.inToken} → native ETH)...`); + const swapOutIsNative = cfg.ooSwap.outToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase(); + const swapOutLabel = swapOutIsNative ? 'ETH' : 'USDC'; + const fmtSwapOut = (v: bigint) => + swapOutIsNative ? ethers.formatEther(v) : ethers.formatUnits(v, 6); + console.log(`Fetching OpenOcean quote (${cfg.ooSwap.inToken} → ${swapOutLabel})...`); const q = await fetchOoQuote(cfg.ooSwap, routerAddress, inputAmount); ooRouter = q.ooRouter; swapData = q.swapData; @@ -509,9 +640,9 @@ async function executeLeg( minAmountOut = q.minAmountOut; console.log(` OO router: ${ooRouter}`); - console.log(` Est. out: ${ethers.formatEther(q.estimatedOut)} ETH`); - console.log(` Fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); - console.log(` Min out: ${ethers.formatEther(minAmountOut)} ETH`); + console.log(` Est. out: ${fmtSwapOut(q.estimatedOut)} ${swapOutLabel}`); + console.log(` Fee: ${fmtSwapOut(feeAmount)} ${swapOutLabel} (${FEE_BPS} bps)`); + console.log(` Min out: ${fmtSwapOut(minAmountOut)} ${swapOutLabel}`); } else { // Case 2: no OO swap — bridge entire balance minus fee feeAmount = bpsOf(inputAmount, FEE_BPS); @@ -553,34 +684,53 @@ async function executeLeg( // Build execution calldata let execCalldata: string; if (useModular) { - const actions = cfg.isNativePool - ? buildNativePoolModularActions( - signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, - minAmountOut, ooRouter, swapData, stargateData, - ) - : buildErc20PoolModularActions( - signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, stargateData, - ); + let actions: ModularAction[]; + if (cfg.isNativePool) { + actions = buildNativePoolModularActions( + signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, + minAmountOut, ooRouter, swapData, stargateData, + ); + } else if (cfg.isNativeInput) { + actions = buildNativeInErc20BridgeModularActions( + signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, + ooRouter, swapData, stargateData, + ); + } else { + actions = buildErc20PoolModularActions( + signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, stargateData, + ); + } execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); } else { - const mono = cfg.isNativePool - ? buildNativePoolMonolithic( - signerAddress, cfg, inputAmount, feeAmount, minAmountOut, - ooRouter, swapData, stargateData, - ) - : buildErc20PoolMonolithic( - signerAddress, cfg, inputAmount, feeAmount, stargateData, nativeFeeWithBuffer, - ); + let mono: MonolithicExecution; + if (cfg.isNativePool) { + mono = buildNativePoolMonolithic( + signerAddress, cfg, inputAmount, feeAmount, minAmountOut, + ooRouter, swapData, stargateData, + ); + } else if (cfg.isNativeInput) { + mono = buildNativeInErc20BridgeMonolithic( + signerAddress, cfg, inputAmount, feeAmount, minAmountOut, + ooRouter, swapData, stargateData, nativeFeeWithBuffer, + ); + } else { + mono = buildErc20PoolMonolithic( + signerAddress, cfg, inputAmount, feeAmount, stargateData, nativeFeeWithBuffer, + ); + } execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); } - // Ensure AH allowance for the input ERC20 - await ensureAllowanceForAllowanceHolder(signer, cfg.inputToken, inputAmount); + // For ERC20 input only: ensure AH has a persistent ERC20 allowance to pull from. + // Native input (isNativeInput) bypasses this — the ETH is forwarded via msg.value. + if (!cfg.isNativeInput) { + await ensureAllowanceForAllowanceHolder(signer, cfg.inputToken, inputAmount); + } - // txValue: - // Native pool: nativeFeeWithBuffer forwarded to give router enough ETH headroom - // ERC20 pool: nativeFeeWithBuffer forwarded so router can pay the LZ fee - const txValue = nativeFeeWithBuffer; + // txValue forwarded to AH.exec → router: + // Native input: inputAmount (for OO swap) + nativeFeeWithBuffer (LZ fee) + // ERC20 input: nativeFeeWithBuffer only (LZ fee) + const txValue = cfg.isNativeInput ? inputAmount + nativeFeeWithBuffer : nativeFeeWithBuffer; console.log(`AllowanceHolder.exec (txValue = ${ethers.formatEther(txValue)} ${nativeSymbol})...`); const receipt = await execViaAH( @@ -617,19 +767,33 @@ async function runCase( const provider = new ethers.JsonRpcProvider(cfg.rpc); const signerOnChain = signer.connect(provider); - const { balance: walletBalance, decimals } = await getWalletErc20Balance( - cfg.inputToken, - signerAddress, - provider, - ); + let walletBalance: bigint; + let decimals: number; + if (cfg.isNativeInput) { + const raw = await provider.getBalance(signerAddress); + if (raw <= NATIVE_INPUT_GAS_RESERVE) { + throw new Error( + `${cfg.name}: native balance ${ethers.formatEther(raw)} ETH is below gas reserve of ${ethers.formatEther(NATIVE_INPUT_GAS_RESERVE)} ETH.`, + ); + } + // Reserve NATIVE_INPUT_GAS_RESERVE for tx gas + LZ nativeFee buffer; use the rest as input. + walletBalance = raw - NATIVE_INPUT_GAS_RESERVE; + decimals = 18; + } else { + ({ balance: walletBalance, decimals } = await getWalletErc20Balance( + cfg.inputToken, + signerAddress, + provider, + )); + } if (walletBalance === 0n) { throw new Error( - `${cfg.name}: signer ${signerAddress} has zero balance of ${cfg.inputToken} on chain ${cfg.sourceChainId}.`, + `${cfg.name}: signer ${signerAddress} has zero usable balance of ${cfg.inputToken} on chain ${cfg.sourceChainId}.`, ); } - // const legAmount = walletBalance / 2n; - const legAmount = walletBalance; + const legAmount = walletBalance / 2n; + // const legAmount = walletBalance; if (legAmount === 0n) { throw new Error(`${cfg.name}: balance too small to split into two halves.`); } @@ -637,7 +801,7 @@ async function runCase( console.log(`Input token balance: ${ethers.formatUnits(walletBalance, decimals)} (${cfg.inputToken})`); console.log(`Per leg: ${ethers.formatUnits(legAmount, decimals)}`); - // await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface); + await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface); console.log('\nSleeping 3s before modular leg...'); await sleep(3000); From c19c288084ad27e7394d6efce3f6a59dd9e93094 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 14 May 2026 22:22:19 +0530 Subject: [PATCH 24/69] feat: native token direct router call without ah --- scripts/e2e/swapBridgeViaStargateNative.ts | 169 ++++++++++++++++++--- scripts/e2e/utils/allowanceHolder.ts | 28 ++++ 2 files changed, 173 insertions(+), 24 deletions(-) diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index cd471ed..b4bf521 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -33,10 +33,23 @@ * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth - * STARGATE_E2E_CASE=4 PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc direct + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc allowance-holder + * STARGATE_E2E_CASE=4 STARGATE_ROUTER_EXEC=direct PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts * - * Router per source chain: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(chainId)` in config.ts. - * Override with `ROUTER_CHAIN_` env when needed. + * Router execution (`argv[3]` overrides `STARGATE_ROUTER_EXEC`): + * + * | Mode | Behaviour | + * |---------------------|-----------| + * | `direct` | Signer sends tx directly to router with `{ value }` | + * | `allowance-holder` | `AllowanceHolder.exec` wraps router (`msg.value` + ERC-2771 user suffix) | + * + * **Native-token input (case 4):** choose explicitly — either pass `direct` or `allowance-holder` as argv[3], + * or set `STARGATE_ROUTER_EXEC`. There is no default; ambiguous runs exit with usage. + * + * **ERC20 input (cases 1–3):** defaults to `allowance-holder`; `direct` is rejected (AH pull required). + * + * Router per source chain: `ROUTER_BY_CHAIN_ID` / `routerAddressForChain(chainId)` in config.ts (`ROUTER_CHAIN_` overrides). */ import axios from 'axios'; import { ethers, parseEther } from 'ethers'; @@ -61,7 +74,7 @@ import { ARBITRUM_LZ_EID, STARGATE_AMOUNT_LD_OFFSET, } from './config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; +import { execViaAH, execDirect, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { encodeApprove, encodeTransfer, @@ -95,7 +108,7 @@ interface CaseConfig { rpc: string; inputToken: string; inputDecimals: number; - /** true when inputToken is native (ETH/POL); skips ERC20 AH allowance and adjusts txValue */ + /** true when inputToken is native (ETH/POL); exec mode must be set explicitly (`direct` | `allowance-holder`) */ isNativeInput: boolean; ooSwap: OoSwapConfig | null; // null → skip OO swap, bridge input token directly stargatePool: string; @@ -220,6 +233,78 @@ function resolveScenarioConfig(): CaseConfig { return CASES[idx]; } +/** How the signer reaches the router: direct `eth_sendTransaction`, or wrapped `AllowanceHolder.exec`. */ +type RouterExecRoute = 'direct' | 'allowance-holder'; + +/** argv[3] / `STARGATE_ROUTER_EXEC` tokens → canonical route. */ +const ROUTER_EXEC_ALIASES: Record = { + direct: 'direct', + dr: 'direct', + router: 'direct', + + 'allowance-holder': 'allowance-holder', + ah: 'allowance-holder', + exec: 'allowance-holder', +}; + +/** + * Resolves execution transport: **`argv[3]` overrides `STARGATE_ROUTER_EXEC`** when non-empty after trim. + * + * - **Native-token input (`isNativeInput`):** caller **must** set `direct` or `allowance-holder` explicitly + * — no silent default — so AH vs signer→router stays a deliberate choice. + * - **ERC20 input:** defaults to `allowance-holder`; `direct` is rejected (`AllowanceHolder.transferFrom` pull). + */ +function resolveRouterExecRoute(cfg: CaseConfig): RouterExecRoute { + const rawArg = typeof process.argv[3] === 'string' ? process.argv[3].trim().toLowerCase() : ''; + const rawEnv = (process.env.STARGATE_ROUTER_EXEC ?? '').trim().toLowerCase(); + const raw = rawArg || rawEnv; + + const resolveExplicit = (): RouterExecRoute | null => { + if (!raw) { + return null; + } + const route = ROUTER_EXEC_ALIASES[raw]; + if (!route) { + console.error( + `Unknown router exec "${raw}". Use argv[3] or STARGATE_ROUTER_EXEC: direct | allowance-holder (aliases dr, router, ah, exec).`, + ); + process.exit(1); + } + return route; + }; + + const route = resolveExplicit(); + if (route !== null) { + if (!cfg.isNativeInput && route === 'direct') { + console.error( + 'ERC20 input cases cannot use direct router txs: `_pullFromUser` invokes AllowanceHolder.transferFrom, which needs the ephemeral allowance set by AH.exec.', + ); + process.exit(1); + } + return route; + } + + if (cfg.isNativeInput) { + console.error( + [ + 'Native-token input scenarios require an explicit router exec mode (no default).', + '', + ' argv[3] STARGATE_ROUTER_EXEC', + ' ----------------------------- ------------------------------', + ' direct direct', + ' allowance-holder (aliases: ah, exec)', + '', + 'Examples:', + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc direct', + ' STARGATE_ROUTER_EXEC=allowance-holder ts-node scripts/e2e/swapBridgeViaStargateNative.ts 4', + ].join('\n'), + ); + process.exit(1); + } + + return 'allowance-holder'; +} + // ─── Shared Stargate ABI ────────────────────────────────────────────────────── /** Minimal Stargate pool ABI fragments — identical for native and ERC20 pools. */ @@ -573,8 +658,9 @@ function buildNativeInErc20BridgeMonolithic( * [4] nativeCall(stargatePool, stargateData, nativeFeeWithBuffer) * .splicePayloadWord(STARGATE_AMOUNT_LD_OFFSET) ← patches amountLD from [3] * - * No AH.transferFrom step — input ETH is already in the router via msg.value forwarded by AH.exec. - * msg.value = inputAmount + nativeFeeWithBuffer (set by executeLeg for isNativeInput cases). + * No AH.transferFrom step — input ETH is already in the router via msg.value (direct + * router tx or AH.exec both forward the same `txValue` to the router). + * msg.value = inputAmount + nativeFeeWithBuffer (see `executeLeg` / `dispatchRouterTransaction`). */ function buildNativeInErc20BridgeModularActions( signer: string, @@ -603,9 +689,45 @@ function buildNativeInErc20BridgeModularActions( // ─── Execution leg ──────────────────────────────────────────────────────────── +/** + * Dispatches tx to router either as a signer→router `{ value }` call or wrapped in + * `AllowanceHolder.exec` (ERC-2771 suffix so `_msgSender()` resolves inside router). + * + * ERC20 `_pullFromUser` requires ephemeral AH allowance ⇒ `allowance-holder` only for non-native inputs. + */ +async function dispatchRouterTransaction( + route: RouterExecRoute, + cfg: CaseConfig, + signer: ethers.Signer, + routerAddress: string, + execCalldata: string, + inputAmount: bigint, + txValue: bigint, + nativeSymbol: string, +): Promise { + if (route === 'direct') { + console.log(`[exec=direct] ${ethers.formatEther(txValue)} ${nativeSymbol}`); + return execDirect(signer, routerAddress, execCalldata, txValue); + } + // allowance-holder — persistent ERC20→AH approval except for pure native pulls + if (!cfg.isNativeInput) { + await ensureAllowanceForAllowanceHolder(signer, cfg.inputToken, inputAmount); + } + console.log(`[exec=allowance-holder] ${ethers.formatEther(txValue)} ${nativeSymbol}`); + return execViaAH( + signer, + routerAddress, + cfg.inputToken, + inputAmount, + routerAddress, + execCalldata, + txValue, + ); +} + /** * Runs one monolithic or modular leg for a case. - * Fetches quotes, builds calldata, ensures AH allowance, and executes. + * Fetches quotes, builds calldata, and executes via {@link dispatchRouterTransaction}. */ async function executeLeg( legLabel: string, @@ -617,6 +739,7 @@ async function executeLeg( provider: ethers.JsonRpcProvider, inputAmount: bigint, routerIface: ethers.Interface, + routerExec: RouterExecRoute, ): Promise { console.log(`\n── ${legLabel} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); @@ -721,26 +844,20 @@ async function executeLeg( execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); } - // For ERC20 input only: ensure AH has a persistent ERC20 allowance to pull from. - // Native input (isNativeInput) bypasses this — the ETH is forwarded via msg.value. - if (!cfg.isNativeInput) { - await ensureAllowanceForAllowanceHolder(signer, cfg.inputToken, inputAmount); - } - - // txValue forwarded to AH.exec → router: - // Native input: inputAmount (for OO swap) + nativeFeeWithBuffer (LZ fee) + // txValue: + // Native input: inputAmount (forwarded to OO) + nativeFeeWithBuffer (LZ fee) // ERC20 input: nativeFeeWithBuffer only (LZ fee) const txValue = cfg.isNativeInput ? inputAmount + nativeFeeWithBuffer : nativeFeeWithBuffer; - console.log(`AllowanceHolder.exec (txValue = ${ethers.formatEther(txValue)} ${nativeSymbol})...`); - const receipt = await execViaAH( + const receipt = await dispatchRouterTransaction( + routerExec, + cfg, signer, routerAddress, - cfg.inputToken, - inputAmount, - routerAddress, execCalldata, + inputAmount, txValue, + nativeSymbol, ); logTxnSummary( @@ -757,12 +874,14 @@ async function runCase( signer: ethers.Wallet, signerAddress: string, routerIface: ethers.Interface, + routerExec: RouterExecRoute, ): Promise { const routerAddress = routerAddressForChain(cfg.sourceChainId); console.log(`\n${'═'.repeat(70)}`); console.log(`CASE: ${cfg.name}`); console.log('═'.repeat(70)); console.log(`Router (chain ${cfg.sourceChainId}): ${routerAddress}`); + console.log(`Router exec route: ${routerExec}`); const provider = new ethers.JsonRpcProvider(cfg.rpc); const signerOnChain = signer.connect(provider); @@ -801,12 +920,12 @@ async function runCase( console.log(`Input token balance: ${ethers.formatUnits(walletBalance, decimals)} (${cfg.inputToken})`); console.log(`Per leg: ${ethers.formatUnits(legAmount, decimals)}`); - await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface); + await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface, routerExec); console.log('\nSleeping 3s before modular leg...'); await sleep(3000); - await executeLeg('2/2', true, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface); + await executeLeg('2/2', true, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface, routerExec); } // ─── Main ───────────────────────────────────────────────────────────────────── @@ -818,6 +937,7 @@ async function main() { } const cfg = resolveScenarioConfig(); + const routerExec = resolveRouterExecRoute(cfg); // Use any provider to create the wallet; the case reconnects via `runCase`. const signer = new ethers.Wallet(privateKey); @@ -827,8 +947,9 @@ async function main() { console.log(`Signer: ${signerAddress}`); console.log(`Router: ${routerAddressForChain(cfg.sourceChainId)} (chain ${cfg.sourceChainId})`); console.log(`Scenario: ${process.argv[2] ?? process.env.STARGATE_E2E_CASE ?? '(resolved)'}`); + console.log(`Exec: ${routerExec} (argv[3] overrides STARGATE_ROUTER_EXEC; required for native input)`); - await runCase(cfg, signer, signerAddress, routerIface); + await runCase(cfg, signer, signerAddress, routerIface, routerExec); console.log('\n✓ Stargate case completed.'); } diff --git a/scripts/e2e/utils/allowanceHolder.ts b/scripts/e2e/utils/allowanceHolder.ts index 6b986a4..3119ba7 100644 --- a/scripts/e2e/utils/allowanceHolder.ts +++ b/scripts/e2e/utils/allowanceHolder.ts @@ -108,3 +108,31 @@ export async function execViaAH( console.log(`Transaction confirmed in block ${receipt.blockNumber}`); return receipt; } + +/** + * Sends a transaction directly to `target` (the router) without routing through + * AllowanceHolder. Use this when the input token is native ETH/POL — the router's + * `_pullFromUser` path for native tokens only checks `msg.value >= amount` and does + * NOT enforce `_msgSender() == user` nor call `AH.transferFrom`. For modular + * execution (`performModularExecution`) there is no `_pullFromUser` at all. + * + * @param signer - EOA signing and paying for the tx + * @param target - Router contract address + * @param callData - Encoded `performExecution` or `performModularExecution` calldata + * @param txValue - ETH to forward (inputAmount + nativeFeeWithBuffer for native input) + */ +export async function execDirect( + signer: Signer, + target: string, + callData: string, + txValue: bigint, +): Promise { + const tx = await signer.sendTransaction({ to: target, data: callData, value: txValue }); + console.log(`Direct router tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error(`Transaction failed: ${tx.hash}`); + } + console.log(`Transaction confirmed in block ${receipt.blockNumber}`); + return receipt; +} From 035958d883e99e03694f278914f8b27affa50e65 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Thu, 14 May 2026 23:02:34 +0530 Subject: [PATCH 25/69] feat: update swapBridgeViaStargateNative for Polygon USDT0 integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modified case 4 to reflect the new flow: Polygon POL → OO swap → Polygon USDT0 → Arbitrum USDT0 via LZ OFT Adapter. - Updated case configurations to include bridgeContract and lzExtraOptions for USDT0. - Adjusted usage examples and scenario aliases to accommodate the new case. --- scripts/e2e/swapBridgeViaStargateNative.ts | 378 +++++++++++++-------- 1 file changed, 239 insertions(+), 139 deletions(-) diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index b4bf521..ad9cc13 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -5,7 +5,7 @@ * Case 1 Arbitrum USDC → OO swap → native ETH → Stargate Native ETH Pool → Base ETH * Case 2 Polygon USDC → (no swap) → Stargate USDC Pool → Base USDC * Case 3 Base USDC → OO swap → native ETH → Stargate Native ETH Pool → Arb ETH - * Case 4 Arbitrum ETH → OO swap → USDC Arb → Stargate USDC Pool → Base USDC + * Case 4 Polygon POL → OO swap → Polygon USDT0 → USDT0 OFT Adapter → Arbitrum USDT0 * * Native-pool mechanics (cases 1 & 3): * send() requires msg.value >= amountLD + nativeFee (StargatePoolNative._assertMessagingFee). @@ -28,13 +28,13 @@ * 1 / arb-usdc-base-eth Arbitrum USDC → OO → native ETH → Stargate native → Base ETH * 2 / polygon-usdc-base Polygon USDC → Stargate USDC pool → Base USDC (no swap) * 3 / base-usdc-arb-eth Base USDC → OO → native ETH → Stargate native → Arbitrum ETH - * 4 / arb-eth-base-usdc Arbitrum ETH → OO → USDC → Stargate USDC pool → Base USDC - * msg.value = inputETH + nativeFee (native input + LZ fee) + * 4 / polygon-pol-usdt0-arb Polygon POL → OO → Polygon USDT0 → LZ OFT Adapter → Arb USDT0 + * msg.value = inputPOL used in OO swap + LZ nativeFee (POL) * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc direct - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc allowance-holder + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb direct + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb allowance-holder * STARGATE_E2E_CASE=4 STARGATE_ROUTER_EXEC=direct PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts * * Router execution (`argv[3]` overrides `STARGATE_ROUTER_EXEC`): @@ -50,10 +50,14 @@ * **ERC20 input (cases 1–3):** defaults to `allowance-holder`; `direct` is rejected (AH pull required). * * Router per source chain: `ROUTER_BY_CHAIN_ID` / `routerAddressForChain(chainId)` in config.ts (`ROUTER_CHAIN_` overrides). + * + * Bridge note: Case 4 uses LayerZero **`send`** on {@link USDT0_OFT_ADAPTER_POLYGON}, not Stargate. + * ABI matches Stargate pool `send`; `lzExtraOptions` uses TYPE_3 executor gas (same as `swapBridgeViaOft.ts`). */ import axios from 'axios'; import { ethers, parseEther } from 'ethers'; import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; dotenv.config(); import { @@ -69,7 +73,7 @@ import { STARGATE_NATIVE_ARB, STARGATE_NATIVE_BASE, STARGATE_USDC_POLYGON, - STARGATE_USDC_ARB, + USDT0_OFT_ADAPTER_POLYGON, BASE_LZ_EID, ARBITRUM_LZ_EID, STARGATE_AMOUNT_LD_OFFSET, @@ -88,6 +92,12 @@ import { MonolithicExecution, NO_FEE, NO_SWAP, ZERO_ADDRESS } from './utils/cont import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; +/** + * LZ extra options for Polygon USDT0 OFT Adapter `send()` (executor gas). + * Mirrors `swapBridgeViaOft.ts`. + */ +const LZ_EXTRA_OPTIONS_POLYGON_USDT0 = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); + // ─── Case configuration ─────────────────────────────────────────────────────── /** @@ -108,10 +118,13 @@ interface CaseConfig { rpc: string; inputToken: string; inputDecimals: number; - /** true when inputToken is native (ETH/POL); exec mode must be set explicitly (`direct` | `allowance-holder`) */ + /** true when inputToken is native (POL/ETH/BNB); exec mode must be set explicitly (`direct` | `allowance-holder`) */ isNativeInput: boolean; ooSwap: OoSwapConfig | null; // null → skip OO swap, bridge input token directly - stargatePool: string; + /** Contract that receives LZ `send` calldata — Stargate pool or LZ OFT adapter (same ABI shape). */ + bridgeContract: string; + /** `extraOptions` in SendParam (`0x` for Stargate pools; encoded TYPE_3 for USDT0 OFT on Polygon). */ + lzExtraOptions: string; isNativePool: boolean; destLzEid: number; } @@ -131,7 +144,8 @@ const CASES: CaseConfig[] = [ chainId: CHAIN_IDS.ARBITRUM, gasPrice: '1', }, - stargatePool: STARGATE_NATIVE_ARB, + bridgeContract: STARGATE_NATIVE_ARB, + lzExtraOptions: '0x', isNativePool: true, destLzEid: BASE_LZ_EID, }, @@ -143,7 +157,8 @@ const CASES: CaseConfig[] = [ inputDecimals: 6, isNativeInput: false, ooSwap: null, // skip OO swap — bridge USDC directly - stargatePool: STARGATE_USDC_POLYGON, + bridgeContract: STARGATE_USDC_POLYGON, + lzExtraOptions: '0x', isNativePool: false, destLzEid: BASE_LZ_EID, }, @@ -161,30 +176,30 @@ const CASES: CaseConfig[] = [ chainId: CHAIN_IDS.BASE, gasPrice: '1', }, - stargatePool: STARGATE_NATIVE_BASE, + bridgeContract: STARGATE_NATIVE_BASE, + lzExtraOptions: '0x', isNativePool: true, destLzEid: ARBITRUM_LZ_EID, }, { - // msg.value = inputETH (swapped via OO) + nativeFeeWithBuffer (LZ fee). - // After the OO swap the router holds USDC + nativeFeeWithBuffer ETH, which it - // uses to pay the Stargate USDC pool's LZ fee. - name: 'Arbitrum ETH → USDC (OO) → Base USDC (Stargate USDC Pool)', - sourceChainId: CHAIN_IDS.ARBITRUM, - rpc: RPC.ARBITRUM, + // Polygon native POL → USDT0 via OpenOcean → Arbitrum USDT0 via USDT0 OFT Adapter on Polygon (LayerZero `send`). + name: 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (LZ OFT Adapter)', + sourceChainId: CHAIN_IDS.POLYGON, + rpc: RPC.POLYGON, inputToken: NATIVE_TOKEN_ADDRESS, inputDecimals: 18, isNativeInput: true, ooSwap: { inToken: NATIVE_TOKEN_ADDRESS, - outToken: TOKENS.USDC_ARB, + outToken: TOKENS.USDT0_POLYGON, inDecimals: 18, - chainId: CHAIN_IDS.ARBITRUM, + chainId: CHAIN_IDS.POLYGON, gasPrice: '1', }, - stargatePool: STARGATE_USDC_ARB, + bridgeContract: USDT0_OFT_ADAPTER_POLYGON, + lzExtraOptions: LZ_EXTRA_OPTIONS_POLYGON_USDT0, isNativePool: false, - destLzEid: BASE_LZ_EID, + destLzEid: ARBITRUM_LZ_EID, }, ]; @@ -204,8 +219,9 @@ const STARGATE_SCENARIO_ALIASES: Record = { 'base-native-arb': 2, '4': 3, - 'arb-eth-base-usdc': 3, - 'arb-native-usdc-base': 3, + 'polygon-pol-usdt0-arb': 3, + 'polygon-pol-arb-usdt0': 3, + 'pol-native-usdt0-arb': 3, }; /** @@ -220,7 +236,7 @@ function resolveScenarioConfig(): CaseConfig { ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth\n' + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-usdc-base\n' + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts base-usdc-arb-eth\n' + - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc\n' + + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb\n' + 'Or use numeric slugs 1 | 2 | 3 | 4.', ); process.exit(1); @@ -295,7 +311,7 @@ function resolveRouterExecRoute(cfg: CaseConfig): RouterExecRoute { ' allowance-holder (aliases: ah, exec)', '', 'Examples:', - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-eth-base-usdc direct', + ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb direct', ' STARGATE_ROUTER_EXEC=allowance-holder ts-node scripts/e2e/swapBridgeViaStargateNative.ts 4', ].join('\n'), ); @@ -322,6 +338,8 @@ interface OoQuoteResponse { data: { to: string; data: string; + /** wei to forward with OO call for native-token sells (omit or "0" for ERC20 sells) */ + value?: string; outAmount: string; minOutAmount: string; }; @@ -336,7 +354,14 @@ async function fetchOoQuote( routerAddress: string, amount: bigint, slippageBps: number = 100, -): Promise<{ ooRouter: string; swapData: string; estimatedOut: bigint; minAmountOut: bigint }> { +): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + /** OO-recommended wei for swap calldata (`value` field); prefer over raw `amount` when > 0 */ + nativeSwapWei: bigint; +}> { const params: Record = { inTokenAddress: cfg.inToken, outTokenAddress: cfg.outToken, @@ -352,11 +377,13 @@ async function fetchOoQuote( const url = `https://open-api.openocean.finance/v3/${cfg.chainId}/swap_quote`; const response = await axios.get(url, { params }); const q = response.data.data; + const nativeSwapWeiRaw = q.value !== undefined && q.value !== '' ? BigInt(q.value) : 0n; return { ooRouter: q.to, swapData: q.data, estimatedOut: BigInt(q.outAmount), minAmountOut: BigInt(q.minOutAmount), + nativeSwapWei: nativeSwapWeiRaw, }; } @@ -370,6 +397,7 @@ async function fetchOoQuote( * @param destLzEid LayerZero destination EID * @param recipient Recipient on destination (also refundAddress) * @param bridgeAmountLD Tentative bridge amount for the quote + * @param extraOptions `SendParam.extraOptions` — `'0x'` for Stargate pools; LZ TYPE_3 for USDT0 OFT adapter */ async function fetchStargateQuote( pool: string, @@ -377,6 +405,7 @@ async function fetchStargateQuote( destLzEid: number, recipient: string, bridgeAmountLD: bigint, + extraOptions: string, ): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract(pool, STARGATE_ABI, provider); const to32 = ethers.zeroPadValue(recipient, 32); @@ -385,7 +414,7 @@ async function fetchStargateQuote( to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, - extraOptions: '0x', + extraOptions, composeMsg: '0x', oftCmd: '0x', }; @@ -412,12 +441,14 @@ async function fetchStargateQuote( * @param nativeFee LZ fee in source-chain native token (with buffer) * @param recipient Recipient address on destination chain * @param amountLD Explicit amountLD (for native pools); 0n for ERC20 pools + * @param extraOptions `SendParam.extraOptions` */ function buildStargateCalldata( destLzEid: number, nativeFee: bigint, recipient: string, amountLD: bigint, + extraOptions: string, ): string { return STARGATE_IFACE.encodeFunctionData('send', [ { @@ -425,7 +456,7 @@ function buildStargateCalldata( to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, - extraOptions: '0x', + extraOptions, composeMsg: '0x', oftCmd: '0x', }, @@ -467,7 +498,7 @@ function buildNativePoolMonolithic( }, postFee: { receiver: signer, amount: feeAmount }, bridge: { - target: cfg.stargatePool, + target: cfg.bridgeContract, approvalSpender: ZERO_ADDRESS, // no ERC20 approval for native ETH value: 0n, // ignored when useFinalAmountAsValue=true data: stargateData, @@ -498,8 +529,8 @@ function buildErc20PoolMonolithic( swap: NO_SWAP, // skip swap — finalToken = inputToken, finalAmount = inputAmount - preFee postFee: { receiver: signer, amount: feeAmount }, bridge: { - target: cfg.stargatePool, - approvalSpender: cfg.stargatePool, // router must approve USDC to pool + target: cfg.bridgeContract, + approvalSpender: cfg.bridgeContract, // router must approve USDC to pool value: nativeFeeWithBuffer, // POL/native forwarded as LZ fee msg.value data: stargateData, amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice at byte 196 @@ -549,7 +580,7 @@ function buildNativePoolModularActions( exec.nativeCall(signer, '0x', feeAmount); // post-swap fee in ETH // Bridge: value = amountLD + nativeFeeWithBuffer = minAmountOut - feeAmount const bridgeValue = minAmountOut - feeAmount; - exec.nativeCall(cfg.stargatePool, stargateData, bridgeValue); + exec.nativeCall(cfg.bridgeContract, stargateData, bridgeValue); return exec.toActions(); } @@ -584,35 +615,39 @@ function buildErc20PoolModularActions( ahIface.encodeFunctionData('transferFrom', [cfg.inputToken, signer, routerAddress, inputAmount]), ); exec.call(cfg.inputToken, encodeTransfer(signer, feeAmount)); // USDC fee to signer - exec.call(cfg.inputToken, encodeApprove(cfg.stargatePool, ethers.MaxUint256)); + exec.call(cfg.inputToken, encodeApprove(cfg.bridgeContract, ethers.MaxUint256)); const usdcBalance = exec.staticCall(cfg.inputToken, encodeBalanceOf(routerAddress)); exec - .nativeCall(cfg.stargatePool, stargateData, nativeFeeWithBuffer) + .nativeCall(cfg.bridgeContract, stargateData, nativeFeeWithBuffer) .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); return exec.toActions(); } /** - * ETH reserved from native balance for gas + LZ fee when inputToken is native. - * The balance read in runCase subtracts this before using the remainder as inputAmount, - * so the signer always has headroom to pay tx gas on top of (inputAmount + nativeFeeWithBuffer). + * Fallback gas reserve used in `runCase` when splitting the balance into leg amounts. + * The actual safety budget used in `executeLeg` is derived dynamically from the + * provider's current `maxFeePerGas` (see `NATIVE_INPUT_GAS_LIMIT_ESTIMATE`). + */ +const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); + +/** + * Estimated gas units for a native-input leg (generous upper bound covering the + * modular path with OO multi-hop swap + LZ OFT send on Polygon). + * Actual usage is ~1M–1.1M; using 2M × maxFeePerGas gives a comfortable ceiling. */ -const NATIVE_INPUT_GAS_RESERVE = parseEther("0.001"); +const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; // ─── Monolithic/modular builders for case 4 ─────────────────────────────────── /** - * Monolithic for case 4 (native ETH input → OO swap to USDC → Stargate USDC pool → Base USDC): + * Monolithic for case 4 (native gas token input → OO swap to bridged ERC-20 → LZ `send` on adapter/pool): * - inputToken = NATIVE_TOKEN_ADDRESS; swap.approvalSpender = 0 (no ERC20 approve needed) - * - swap.value = inputAmount: forwards that ETH to OO which returns USDC to the router - * - postFee: router sends feeAmount USDC to signer - * - bridge.approvalSpender = stargatePool: router approves remaining USDC to Stargate - * - bridge.value = nativeFeeWithBuffer: only LZ fee in native (not the USDC bridge amount) - * - amountPositions=[196n]: router splices post-fee USDC finalAmount into stargateData.amountLD + * - swap.value = `ooSwapNativeWei` (OpenOcean `value` field when present; else quoted input wei) + * - postFee: router sends feeAmount of OO output token (e.g. USDT0) to signer + * - bridge: ERC-20 pool mechanics — splice post-fee balance into amountLD at offset 196 * - * msg.value = inputAmount + nativeFeeWithBuffer: - * OO consumes inputAmount ETH → router holds USDC + nativeFeeWithBuffer ETH for the LZ fee. + * msg.value ≈ ooSwapNativeWei + nativeFeeWithBuffer (signer attaches full `txValue`; OO consumes POL/ETH swap leg). */ function buildNativeInErc20BridgeMonolithic( signer: string, @@ -624,43 +659,40 @@ function buildNativeInErc20BridgeMonolithic( swapData: string, stargateData: string, nativeFeeWithBuffer: bigint, + ooSwapNativeWei: bigint, ): MonolithicExecution { + const rawOoWei = ooSwapNativeWei > 0n ? ooSwapNativeWei : inputAmount; + const polOrEthToOo = rawOoWei <= inputAmount ? rawOoWei : inputAmount; return { input: { user: signer, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, preFee: NO_FEE, swap: { target: ooRouter, approvalSpender: ZERO_ADDRESS, // no ERC20 approve for native ETH input - outputToken: cfg.ooSwap!.outToken, // USDC_ARB - value: inputAmount, // forward inputAmount ETH to OO + outputToken: cfg.ooSwap!.outToken, + value: polOrEthToOo, minOutput: minAmountOut, data: swapData, returnDataWordOffset: 0n, }, - postFee: { receiver: signer, amount: feeAmount }, // fee in USDC + postFee: { receiver: signer, amount: feeAmount }, // fee in OO output token (USDC/USDT0) bridge: { - target: cfg.stargatePool, - approvalSpender: cfg.stargatePool, // router approves USDC to Stargate pool - value: nativeFeeWithBuffer, // LZ fee only; not the USDC bridge amount + target: cfg.bridgeContract, + approvalSpender: cfg.bridgeContract, // router approves bridge contract to pull ERC20 + value: nativeFeeWithBuffer, // LZ fee in native gas token only data: stargateData, - amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice USDC amountLD at runtime + amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice amountLD at runtime useFinalAmountAsValue: false, }, }; } /** - * Modular for case 4 (native ETH input → OO swap to USDC → Stargate USDC pool → Base USDC): - * [0] nativeCall(ooRouter, swapData, inputAmount) — send inputAmount ETH to OO, get USDC - * [1] USDC.transfer(signer, feeAmount) — fee out to signer - * [2] USDC.approve(stargatePool, MaxUint256) - * [3] STATICCALL USDC.balanceOf(router) → stored for splice into [4] - * [4] nativeCall(stargatePool, stargateData, nativeFeeWithBuffer) - * .splicePayloadWord(STARGATE_AMOUNT_LD_OFFSET) ← patches amountLD from [3] + * Modular for case 4 (native gas token in → OO → ERC-20 out → LZ `send`): + * [0] nativeCall(ooRouter, swapData, ooSwapWei) — OO `value` when present else leg `inputAmount` + * … same ERC-20 fee / approve / splice as monolithic ERC20-pool bridge path. * - * No AH.transferFrom step — input ETH is already in the router via msg.value (direct - * router tx or AH.exec both forward the same `txValue` to the router). - * msg.value = inputAmount + nativeFeeWithBuffer (see `executeLeg` / `dispatchRouterTransaction`). + * Input native is forwarded on the enclosing tx (`txValue`); no AH.transferFrom pull. */ function buildNativeInErc20BridgeModularActions( signer: string, @@ -672,17 +704,20 @@ function buildNativeInErc20BridgeModularActions( ooRouter: string, swapData: string, stargateData: string, + ooSwapNativeWei: bigint, ): ModularAction[] { const exec = new ModularActionsBuilder(); - const usdcToken = cfg.ooSwap!.outToken; // USDC_ARB - - exec.nativeCall(ooRouter, swapData, inputAmount); // ETH → USDC, USDC lands in router - exec.call(usdcToken, encodeTransfer(signer, feeAmount)); - exec.call(usdcToken, encodeApprove(cfg.stargatePool, ethers.MaxUint256)); - const usdcBalance = exec.staticCall(usdcToken, encodeBalanceOf(routerAddress)); + const outTokenAddr = cfg.ooSwap!.outToken; + const rawOoWei = ooSwapNativeWei > 0n ? ooSwapNativeWei : inputAmount; + const polOrEthToOo = rawOoWei <= inputAmount ? rawOoWei : inputAmount; + + exec.nativeCall(ooRouter, swapData, polOrEthToOo); + exec.call(outTokenAddr, encodeTransfer(signer, feeAmount)); + exec.call(outTokenAddr, encodeApprove(cfg.bridgeContract, ethers.MaxUint256)); + const tokenBal = exec.staticCall(outTokenAddr, encodeBalanceOf(routerAddress)); exec - .nativeCall(cfg.stargatePool, stargateData, nativeFeeWithBuffer) - .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); + .nativeCall(cfg.bridgeContract, stargateData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), tokenBal.returnWord()); return exec.toActions(); } @@ -743,84 +778,156 @@ async function executeLeg( ): Promise { console.log(`\n── ${legLabel} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - let feeAmount: bigint; + const nativeSymbol = cfg.sourceChainId === CHAIN_IDS.POLYGON ? 'POL' : 'ETH'; + + let inputAmountWei = inputAmount; + + let feeAmount = 0n; let minAmountOut = 0n; - let estimatedBridgeAmount: bigint; + let estimatedBridgeAmount = 0n; let ooRouter = ''; let swapData = ''; - - if (cfg.ooSwap !== null) { - const swapOutIsNative = cfg.ooSwap.outToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase(); - const swapOutLabel = swapOutIsNative ? 'ETH' : 'USDC'; - const fmtSwapOut = (v: bigint) => - swapOutIsNative ? ethers.formatEther(v) : ethers.formatUnits(v, 6); - console.log(`Fetching OpenOcean quote (${cfg.ooSwap.inToken} → ${swapOutLabel})...`); - const q = await fetchOoQuote(cfg.ooSwap, routerAddress, inputAmount); - ooRouter = q.ooRouter; - swapData = q.swapData; - feeAmount = bpsOf(q.estimatedOut, FEE_BPS); - estimatedBridgeAmount = q.estimatedOut - feeAmount; - minAmountOut = q.minAmountOut; - - console.log(` OO router: ${ooRouter}`); - console.log(` Est. out: ${fmtSwapOut(q.estimatedOut)} ${swapOutLabel}`); - console.log(` Fee: ${fmtSwapOut(feeAmount)} ${swapOutLabel} (${FEE_BPS} bps)`); - console.log(` Min out: ${fmtSwapOut(minAmountOut)} ${swapOutLabel}`); - } else { - // Case 2: no OO swap — bridge entire balance minus fee - feeAmount = bpsOf(inputAmount, FEE_BPS); - estimatedBridgeAmount = inputAmount - feeAmount; - console.log(` Fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + let ooSwapNativeWei = 0n; + let nativeFeeWithBuffer = 0n; + let amountReceivedLD = 0n; + /** Last raw quote fee (logged before buffer). */ + let nativeFeeQuoted = 0n; + + /** + * Dynamic gas reserve for native-input cases: current maxFeePerGas × generous gas limit estimate. + * Fetched once before the loop so we don't hammer the RPC on each cap iteration. + * Falls back to a hardcoded minimum if fee data is unavailable. + */ + let gasReserve = NATIVE_INPUT_GAS_RESERVE; + if (cfg.isNativeInput) { + const feeData = await provider.getFeeData(); + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice ?? 500_000_000n; + gasReserve = maxFeePerGas * NATIVE_INPUT_GAS_LIMIT_ESTIMATE; + console.log( + ` Gas reserve (${NATIVE_INPUT_GAS_LIMIT_ESTIMATE / 1_000_000n}M gas × ${ethers.formatUnits(maxFeePerGas, 'gwei')} Gwei): ` + + `${ethers.formatEther(gasReserve)} ${nativeSymbol}`, + ); } - // Fetch Stargate quote for nativeFee and expected receive amount - console.log(`Fetching Stargate quoteSend (pool ${cfg.stargatePool})...`); - const { nativeFee, amountReceivedLD } = await fetchStargateQuote( - cfg.stargatePool, - provider, - cfg.destLzEid, - signerAddress, - estimatedBridgeAmount, - ); - const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + const MAX_NATIVE_INPUT_CAP_ITER = 6; - const nativeSymbol = cfg.sourceChainId === CHAIN_IDS.POLYGON ? 'POL' : 'ETH'; - console.log(` nativeFee: ${ethers.formatEther(nativeFee)} ${nativeSymbol}`); - console.log(` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} ${nativeSymbol}`); - console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, cfg.isNativePool ? 18 : 6)}`); + let iter = 0; + for (;;) { + iter++; + if (iter > MAX_NATIVE_INPUT_CAP_ITER) { + throw new Error( + `${cfg.name}: native swap budgeting hit ${MAX_NATIVE_INPUT_CAP_ITER} re-quote iterations; top up native balance or widen gas reserve.`, + ); + } + + if (cfg.ooSwap !== null) { + const swapOutIsNative = cfg.ooSwap.outToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase(); + const swapOutIsUsdt0 = cfg.ooSwap.outToken.toLowerCase() === TOKENS.USDT0_POLYGON.toLowerCase(); + const swapOutLabel = swapOutIsNative + ? cfg.sourceChainId === CHAIN_IDS.POLYGON + ? 'POL' + : 'ETH' + : swapOutIsUsdt0 + ? 'USDT0' + : 'USDC'; + const swapOutDecimals = swapOutIsNative ? 18 : 6; + const fmtSwapOut = (v: bigint) => + swapOutIsNative ? ethers.formatEther(v) : ethers.formatUnits(v, swapOutDecimals); + + console.log(`Fetching OpenOcean quote (${cfg.ooSwap.inToken} → ${swapOutLabel})...`); + const q = await fetchOoQuote(cfg.ooSwap, routerAddress, inputAmountWei); + ooRouter = q.ooRouter; + swapData = q.swapData; + ooSwapNativeWei = q.nativeSwapWei; + feeAmount = bpsOf(q.estimatedOut, FEE_BPS); + estimatedBridgeAmount = q.estimatedOut - feeAmount; + minAmountOut = q.minAmountOut; + + console.log(` OO router: ${ooRouter}`); + console.log(` Est. out: ${fmtSwapOut(q.estimatedOut)} ${swapOutLabel}`); + console.log(` Fee: ${fmtSwapOut(feeAmount)} ${swapOutLabel} (${FEE_BPS} bps)`); + console.log(` Min out: ${fmtSwapOut(minAmountOut)} ${swapOutLabel}`); + if (ooSwapNativeWei > 0n) { + console.log(` OO swap value wei: ${ooSwapNativeWei.toString()} (attached to OO call)`); + } + } else { + // Case 2: no OO swap — bridge entire balance minus fee + feeAmount = bpsOf(inputAmountWei, FEE_BPS); + estimatedBridgeAmount = inputAmountWei - feeAmount; + console.log(` Fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + } + + console.log(`Fetching bridge quoteSend (${cfg.bridgeContract}, extraOpts ${cfg.lzExtraOptions.slice(0, 18)}...) ...`); + const lzQuote = await fetchStargateQuote( + cfg.bridgeContract, + provider, + cfg.destLzEid, + signerAddress, + estimatedBridgeAmount, + cfg.lzExtraOptions, + ); + nativeFeeQuoted = lzQuote.nativeFee; + nativeFeeWithBuffer = (lzQuote.nativeFee * 105n) / 100n; + amountReceivedLD = lzQuote.amountReceivedLD; + + console.log(` nativeFee: ${ethers.formatEther(nativeFeeQuoted)} ${nativeSymbol}`); + console.log(` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} ${nativeSymbol}`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, cfg.isNativePool ? 18 : 6)}`); + + if (!cfg.isNativeInput) { + break; + } + + const balNow = await provider.getBalance(signerAddress); + // maxAffordableSwapIn = balance we can put into the swap leg so that + // txValue (= inputAmountWei + nativeFeeWithBuffer) + gas cost ≤ balance + const maxAffordableSwapIn = balNow - nativeFeeWithBuffer - gasReserve; + if (maxAffordableSwapIn <= 0n) { + throw new Error( + `${cfg.name}: signer native balance (${ethers.formatEther(balNow)} ${nativeSymbol}) cannot cover lz fee ` + + `(${ethers.formatEther(nativeFeeWithBuffer)} ${nativeSymbol}) plus gas reserve ` + + `(${ethers.formatEther(gasReserve)} ${nativeSymbol}).`, + ); + } + if (inputAmountWei <= maxAffordableSwapIn) { + break; + } + + console.warn( + `[${legLabel}] capping ${nativeSymbol} swap input: planned ${ethers.formatEther(inputAmountWei)} ` + + `exceeds max affordable ${ethers.formatEther(maxAffordableSwapIn)} (balance − lz fee − gas reserve). Re-quoting.`, + ); + inputAmountWei = maxAffordableSwapIn; + } - // Build Stargate calldata let amountLD: bigint; if (cfg.isNativePool) { - // Use minAmountOut as the basis so that msg.value (= actualFinalAmount) >= amountLD + nativeFeeWithBuffer - // is always satisfied: since actual >= min is guaranteed by OO slippage, - // actual - fee >= min - fee = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee ✓ amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; if (amountLD <= 0n) { throw new Error(`${cfg.name}: minAmountOut too small to cover fee + nativeFee.`); } } else { - amountLD = 0n; // placeholder — spliced by amountPositions or spliceWord at runtime + amountLD = 0n; } - const stargateData = buildStargateCalldata(cfg.destLzEid, nativeFeeWithBuffer, signerAddress, amountLD); - // Build execution calldata + const stargateData = buildStargateCalldata(cfg.destLzEid, nativeFeeWithBuffer, signerAddress, amountLD, cfg.lzExtraOptions); + let execCalldata: string; if (useModular) { let actions: ModularAction[]; if (cfg.isNativePool) { actions = buildNativePoolModularActions( - signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, + signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, minAmountOut, ooRouter, swapData, stargateData, ); } else if (cfg.isNativeInput) { actions = buildNativeInErc20BridgeModularActions( - signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, - ooRouter, swapData, stargateData, + signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, + ooRouter, swapData, stargateData, ooSwapNativeWei, ); } else { actions = buildErc20PoolModularActions( - signerAddress, routerAddress, cfg, inputAmount, feeAmount, nativeFeeWithBuffer, stargateData, + signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, stargateData, ); } execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); @@ -828,26 +935,23 @@ async function executeLeg( let mono: MonolithicExecution; if (cfg.isNativePool) { mono = buildNativePoolMonolithic( - signerAddress, cfg, inputAmount, feeAmount, minAmountOut, + signerAddress, cfg, inputAmountWei, feeAmount, minAmountOut, ooRouter, swapData, stargateData, ); } else if (cfg.isNativeInput) { mono = buildNativeInErc20BridgeMonolithic( - signerAddress, cfg, inputAmount, feeAmount, minAmountOut, - ooRouter, swapData, stargateData, nativeFeeWithBuffer, + signerAddress, cfg, inputAmountWei, feeAmount, minAmountOut, + ooRouter, swapData, stargateData, nativeFeeWithBuffer, ooSwapNativeWei, ); } else { mono = buildErc20PoolMonolithic( - signerAddress, cfg, inputAmount, feeAmount, stargateData, nativeFeeWithBuffer, + signerAddress, cfg, inputAmountWei, feeAmount, stargateData, nativeFeeWithBuffer, ); } execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); } - // txValue: - // Native input: inputAmount (forwarded to OO) + nativeFeeWithBuffer (LZ fee) - // ERC20 input: nativeFeeWithBuffer only (LZ fee) - const txValue = cfg.isNativeInput ? inputAmount + nativeFeeWithBuffer : nativeFeeWithBuffer; + const txValue = cfg.isNativeInput ? inputAmountWei + nativeFeeWithBuffer : nativeFeeWithBuffer; const receipt = await dispatchRouterTransaction( routerExec, @@ -855,16 +959,12 @@ async function executeLeg( signer, routerAddress, execCalldata, - inputAmount, + inputAmountWei, txValue, nativeSymbol, ); - logTxnSummary( - `${cfg.name} — ${useModular ? 'Modular' : 'Monolithic'}`, - cfg.sourceChainId, - receipt, - ); + logTxnSummary(`${cfg.name} — ${useModular ? 'Modular' : 'Monolithic'}`, cfg.sourceChainId, receipt); } // ─── Run one case (monolithic + sleep + modular) ────────────────────────────── @@ -891,11 +991,12 @@ async function runCase( if (cfg.isNativeInput) { const raw = await provider.getBalance(signerAddress); if (raw <= NATIVE_INPUT_GAS_RESERVE) { + const sym = cfg.sourceChainId === CHAIN_IDS.POLYGON ? 'POL' : 'ETH'; throw new Error( - `${cfg.name}: native balance ${ethers.formatEther(raw)} ETH is below gas reserve of ${ethers.formatEther(NATIVE_INPUT_GAS_RESERVE)} ETH.`, + `${cfg.name}: native balance ${ethers.formatEther(raw)} ${sym} is below reserve of ${ethers.formatEther(NATIVE_INPUT_GAS_RESERVE)} ${sym}.`, ); } - // Reserve NATIVE_INPUT_GAS_RESERVE for tx gas + LZ nativeFee buffer; use the rest as input. + // Reserve wei for signer gas; lz fee itself is deducted inside executeLeg (`txValue = swap + fee`). walletBalance = raw - NATIVE_INPUT_GAS_RESERVE; decimals = 18; } else { @@ -912,13 +1013,12 @@ async function runCase( } const legAmount = walletBalance / 2n; - // const legAmount = walletBalance; if (legAmount === 0n) { throw new Error(`${cfg.name}: balance too small to split into two halves.`); } console.log(`Input token balance: ${ethers.formatUnits(walletBalance, decimals)} (${cfg.inputToken})`); - console.log(`Per leg: ${ethers.formatUnits(legAmount, decimals)}`); + console.log(`Per leg (½): ${ethers.formatUnits(legAmount, decimals)}`); await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface, routerExec); From c8a5492505d731ec61cfce86a836a2a030c4d10d Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 01:52:49 +0530 Subject: [PATCH 26/69] fix: build --- .npmrc | 4 + package-lock.json | 70 +- yarn.lock | 2139 --------------------------------------------- 3 files changed, 44 insertions(+), 2169 deletions(-) create mode 100644 .npmrc delete mode 100644 yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4885094 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +# Only resolve dependency versions published at least this many days ago. +# Helps limit supply-chain risk from freshly published malicious releases. +# Supported in npm 11+ (see `npm config get min-release-age`). +min-release-age=14 diff --git a/package-lock.json b/package-lock.json index a4813d2..53d4d7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "poc-openrouter", "version": "1.0.0", + "dependencies": { + "@layerzerolabs/lz-v2-utilities": "^3.0.168" + }, "devDependencies": { "@arbitrum/sdk": "^4.0.5", "@nomicfoundation/hardhat-foundry": "^1.1.2", @@ -177,7 +180,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", - "dev": true, "funding": [ { "type": "individual", @@ -205,7 +207,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", - "dev": true, "funding": [ { "type": "individual", @@ -231,7 +232,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", - "dev": true, "funding": [ { "type": "individual", @@ -255,7 +255,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", - "dev": true, "funding": [ { "type": "individual", @@ -279,7 +278,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", - "dev": true, "funding": [ { "type": "individual", @@ -320,7 +318,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", - "dev": true, "funding": [ { "type": "individual", @@ -342,7 +339,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", - "dev": true, "funding": [ { "type": "individual", @@ -362,7 +358,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", - "dev": true, "funding": [ { "type": "individual", @@ -411,7 +406,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", - "dev": true, "funding": [ { "type": "individual", @@ -509,7 +503,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", - "dev": true, "funding": [ { "type": "individual", @@ -530,7 +523,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", - "dev": true, "funding": [ { "type": "individual", @@ -547,7 +539,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", - "dev": true, "funding": [ { "type": "individual", @@ -588,7 +579,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", - "dev": true, "funding": [ { "type": "individual", @@ -690,7 +680,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", - "dev": true, "funding": [ { "type": "individual", @@ -711,7 +700,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", - "dev": true, "funding": [ { "type": "individual", @@ -733,7 +721,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", - "dev": true, "funding": [ { "type": "individual", @@ -758,7 +745,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", - "dev": true, "funding": [ { "type": "individual", @@ -783,7 +769,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", - "dev": true, "funding": [ { "type": "individual", @@ -805,7 +790,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", - "dev": true, "funding": [ { "type": "individual", @@ -889,7 +873,6 @@ "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", - "dev": true, "funding": [ { "type": "individual", @@ -971,6 +954,37 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@layerzerolabs/lz-v2-utilities": { + "version": "3.0.168", + "resolved": "https://registry.npmjs.org/@layerzerolabs/lz-v2-utilities/-/lz-v2-utilities-3.0.168.tgz", + "integrity": "sha512-5gb5QH3q+JIOkwuJnmv3hWidwLE7ySC0G4IYCL9pwl80bkdkuY9TwfG3KqWri2F5mCzRYs6xx7vaYP5zFqY7yA==", + "license": "BUSL-1.1", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/solidity": "^5.8.0", + "bs58": "^5.0.0", + "tiny-invariant": "^1.3.1" + } + }, + "node_modules/@layerzerolabs/lz-v2-utilities/node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/@layerzerolabs/lz-v2-utilities/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, "node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -2302,7 +2316,6 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", - "dev": true, "license": "MIT" }, "node_modules/boxen": { @@ -2368,7 +2381,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true, "license": "MIT" }, "node_modules/browser-stdout": { @@ -3266,7 +3278,6 @@ "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.11.9", @@ -3282,7 +3293,6 @@ "version": "4.12.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { @@ -4682,7 +4692,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -4724,7 +4733,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, "license": "MIT", "dependencies": { "hash.js": "^1.0.3", @@ -4872,7 +4880,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -5046,7 +5053,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5433,14 +5439,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true, "license": "MIT" }, "node_modules/minimatch": { @@ -7334,6 +7338,12 @@ "readable-stream": "3" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 7ffd61c..0000000 --- a/yarn.lock +++ /dev/null @@ -1,2139 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@adraffy/ens-normalize@1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" - integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== - -"@arbitrum/sdk@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-4.0.5.tgz#c7abf6fcec72b36faee7af704245a7e14c49ad7f" - integrity sha512-bADi4kVzSBUAV+GkxuKMx7zrkCVahIE4+fkBi0Ee18EPqGt1Wiub+yQCGTh+llApn1RpRtwwtYeZXhz9XelqGQ== - dependencies: - "@ethersproject/address" "^5.0.8" - "@ethersproject/bignumber" "^5.1.1" - "@ethersproject/bytes" "^5.0.8" - async-mutex "^0.4.0" - ethers "^5.1.0" - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@ethereumjs/rlp@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842" - integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA== - -"@ethereumjs/util@^9.1.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-9.1.0.tgz#75e3898a3116d21c135fa9e29886565609129bce" - integrity sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog== - dependencies: - "@ethereumjs/rlp" "^5.0.2" - ethereum-cryptography "^2.2.1" - -"@ethersproject/abi@5.8.0", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.8.0.tgz#e79bb51940ac35fe6f3262d7fe2cdb25ad5f07d9" - integrity sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q== - dependencies: - "@ethersproject/address" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/constants" "^5.8.0" - "@ethersproject/hash" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - -"@ethersproject/abstract-provider@5.8.0", "@ethersproject/abstract-provider@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz#7581f9be601afa1d02b95d26b9d9840926a35b0c" - integrity sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg== - dependencies: - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/networks" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/transactions" "^5.8.0" - "@ethersproject/web" "^5.8.0" - -"@ethersproject/abstract-signer@5.8.0", "@ethersproject/abstract-signer@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz#8d7417e95e4094c1797a9762e6789c7356db0754" - integrity sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA== - dependencies: - "@ethersproject/abstract-provider" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - -"@ethersproject/address@5.8.0", "@ethersproject/address@^5.0.8", "@ethersproject/address@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.8.0.tgz#3007a2c352eee566ad745dca1dbbebdb50a6a983" - integrity sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA== - dependencies: - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/rlp" "^5.8.0" - -"@ethersproject/base64@5.8.0", "@ethersproject/base64@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.8.0.tgz#61c669c648f6e6aad002c228465d52ac93ee83eb" - integrity sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ== - dependencies: - "@ethersproject/bytes" "^5.8.0" - -"@ethersproject/basex@5.8.0", "@ethersproject/basex@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.8.0.tgz#1d279a90c4be84d1c1139114a1f844869e57d03a" - integrity sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - -"@ethersproject/bignumber@5.8.0", "@ethersproject/bignumber@^5.1.1", "@ethersproject/bignumber@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.8.0.tgz#c381d178f9eeb370923d389284efa19f69efa5d7" - integrity sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - bn.js "^5.2.1" - -"@ethersproject/bytes@5.8.0", "@ethersproject/bytes@^5.0.8", "@ethersproject/bytes@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.8.0.tgz#9074820e1cac7507a34372cadeb035461463be34" - integrity sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A== - dependencies: - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/constants@5.8.0", "@ethersproject/constants@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.8.0.tgz#12f31c2f4317b113a4c19de94e50933648c90704" - integrity sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg== - dependencies: - "@ethersproject/bignumber" "^5.8.0" - -"@ethersproject/contracts@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.8.0.tgz#243a38a2e4aa3e757215ea64e276f8a8c9d8ed73" - integrity sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ== - dependencies: - "@ethersproject/abi" "^5.8.0" - "@ethersproject/abstract-provider" "^5.8.0" - "@ethersproject/abstract-signer" "^5.8.0" - "@ethersproject/address" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/constants" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/transactions" "^5.8.0" - -"@ethersproject/hash@5.8.0", "@ethersproject/hash@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.8.0.tgz#b8893d4629b7f8462a90102572f8cd65a0192b4c" - integrity sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA== - dependencies: - "@ethersproject/abstract-signer" "^5.8.0" - "@ethersproject/address" "^5.8.0" - "@ethersproject/base64" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - -"@ethersproject/hdnode@5.8.0", "@ethersproject/hdnode@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.8.0.tgz#a51ae2a50bcd48ef6fd108c64cbae5e6ff34a761" - integrity sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA== - dependencies: - "@ethersproject/abstract-signer" "^5.8.0" - "@ethersproject/basex" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/pbkdf2" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/sha2" "^5.8.0" - "@ethersproject/signing-key" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - "@ethersproject/transactions" "^5.8.0" - "@ethersproject/wordlists" "^5.8.0" - -"@ethersproject/json-wallets@5.8.0", "@ethersproject/json-wallets@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz#d18de0a4cf0f185f232eb3c17d5e0744d97eb8c9" - integrity sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w== - dependencies: - "@ethersproject/abstract-signer" "^5.8.0" - "@ethersproject/address" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/hdnode" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/pbkdf2" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/random" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - "@ethersproject/transactions" "^5.8.0" - aes-js "3.0.0" - scrypt-js "3.0.1" - -"@ethersproject/keccak256@5.8.0", "@ethersproject/keccak256@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.8.0.tgz#d2123a379567faf2d75d2aaea074ffd4df349e6a" - integrity sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng== - dependencies: - "@ethersproject/bytes" "^5.8.0" - js-sha3 "0.8.0" - -"@ethersproject/logger@5.8.0", "@ethersproject/logger@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.8.0.tgz#f0232968a4f87d29623a0481690a2732662713d6" - integrity sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA== - -"@ethersproject/networks@5.8.0", "@ethersproject/networks@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.8.0.tgz#8b4517a3139380cba9fb00b63ffad0a979671fde" - integrity sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg== - dependencies: - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/pbkdf2@5.8.0", "@ethersproject/pbkdf2@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz#cd2621130e5dd51f6a0172e63a6e4a0c0a0ec37e" - integrity sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/sha2" "^5.8.0" - -"@ethersproject/properties@5.8.0", "@ethersproject/properties@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.8.0.tgz#405a8affb6311a49a91dabd96aeeae24f477020e" - integrity sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw== - dependencies: - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/providers@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.8.0.tgz#6c2ae354f7f96ee150439f7de06236928bc04cb4" - integrity sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw== - dependencies: - "@ethersproject/abstract-provider" "^5.8.0" - "@ethersproject/abstract-signer" "^5.8.0" - "@ethersproject/address" "^5.8.0" - "@ethersproject/base64" "^5.8.0" - "@ethersproject/basex" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/constants" "^5.8.0" - "@ethersproject/hash" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/networks" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/random" "^5.8.0" - "@ethersproject/rlp" "^5.8.0" - "@ethersproject/sha2" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - "@ethersproject/transactions" "^5.8.0" - "@ethersproject/web" "^5.8.0" - bech32 "1.1.4" - ws "8.18.0" - -"@ethersproject/random@5.8.0", "@ethersproject/random@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.8.0.tgz#1bced04d49449f37c6437c701735a1a022f0057a" - integrity sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/rlp@5.8.0", "@ethersproject/rlp@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.8.0.tgz#5a0d49f61bc53e051532a5179472779141451de5" - integrity sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/sha2@5.8.0", "@ethersproject/sha2@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.8.0.tgz#8954a613bb78dac9b46829c0a95de561ef74e5e1" - integrity sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - hash.js "1.1.7" - -"@ethersproject/signing-key@5.8.0", "@ethersproject/signing-key@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.8.0.tgz#9797e02c717b68239c6349394ea85febf8893119" - integrity sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - bn.js "^5.2.1" - elliptic "6.6.1" - hash.js "1.1.7" - -"@ethersproject/solidity@5.8.0", "@ethersproject/solidity@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.8.0.tgz#429bb9fcf5521307a9448d7358c26b93695379b9" - integrity sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA== - dependencies: - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/sha2" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - -"@ethersproject/strings@5.8.0", "@ethersproject/strings@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.8.0.tgz#ad79fafbf0bd272d9765603215ac74fd7953908f" - integrity sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/constants" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/transactions@5.8.0", "@ethersproject/transactions@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.8.0.tgz#1e518822403abc99def5a043d1c6f6fe0007e46b" - integrity sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg== - dependencies: - "@ethersproject/address" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/constants" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/rlp" "^5.8.0" - "@ethersproject/signing-key" "^5.8.0" - -"@ethersproject/units@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.8.0.tgz#c12f34ba7c3a2de0e9fa0ed0ee32f3e46c5c2c6a" - integrity sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ== - dependencies: - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/constants" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - -"@ethersproject/wallet@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.8.0.tgz#49c300d10872e6986d953e8310dc33d440da8127" - integrity sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA== - dependencies: - "@ethersproject/abstract-provider" "^5.8.0" - "@ethersproject/abstract-signer" "^5.8.0" - "@ethersproject/address" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/hash" "^5.8.0" - "@ethersproject/hdnode" "^5.8.0" - "@ethersproject/json-wallets" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/random" "^5.8.0" - "@ethersproject/signing-key" "^5.8.0" - "@ethersproject/transactions" "^5.8.0" - "@ethersproject/wordlists" "^5.8.0" - -"@ethersproject/web@5.8.0", "@ethersproject/web@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.8.0.tgz#3e54badc0013b7a801463a7008a87988efce8a37" - integrity sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw== - dependencies: - "@ethersproject/base64" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - -"@ethersproject/wordlists@5.8.0", "@ethersproject/wordlists@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.8.0.tgz#7a5654ee8d1bb1f4dbe43f91d217356d650ad821" - integrity sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg== - dependencies: - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/hash" "^5.8.0" - "@ethersproject/logger" "^5.8.0" - "@ethersproject/properties" "^5.8.0" - "@ethersproject/strings" "^5.8.0" - -"@fastify/busboy@^2.0.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" - integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" - integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@layerzerolabs/lz-v2-utilities@^3.0.168": - version "3.0.168" - resolved "https://registry.yarnpkg.com/@layerzerolabs/lz-v2-utilities/-/lz-v2-utilities-3.0.168.tgz#a7b62a1422f978151ca0f3fc77e3ab511f9e6eb3" - integrity sha512-5gb5QH3q+JIOkwuJnmv3hWidwLE7ySC0G4IYCL9pwl80bkdkuY9TwfG3KqWri2F5mCzRYs6xx7vaYP5zFqY7yA== - dependencies: - "@ethersproject/abi" "^5.8.0" - "@ethersproject/address" "^5.8.0" - "@ethersproject/bignumber" "^5.8.0" - "@ethersproject/bytes" "^5.8.0" - "@ethersproject/keccak256" "^5.8.0" - "@ethersproject/solidity" "^5.8.0" - bs58 "^5.0.0" - tiny-invariant "^1.3.1" - -"@noble/curves@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== - dependencies: - "@noble/hashes" "1.3.2" - -"@noble/curves@1.4.2", "@noble/curves@~1.4.0": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" - integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== - dependencies: - "@noble/hashes" "1.4.0" - -"@noble/curves@~1.8.1": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.2.tgz#8f24c037795e22b90ae29e222a856294c1d9ffc7" - integrity sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g== - dependencies: - "@noble/hashes" "1.7.2" - -"@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" - integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== - -"@noble/hashes@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" - integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== - -"@noble/hashes@1.4.0", "@noble/hashes@~1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" - integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== - -"@noble/hashes@1.7.2", "@noble/hashes@~1.7.1": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" - integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== - -"@noble/secp256k1@1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" - integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== - -"@noble/secp256k1@~1.7.0": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.2.tgz#c2c3343e2dce80e15a914d7442147507f8a98e7f" - integrity sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ== - -"@nomicfoundation/edr-darwin-arm64@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.23.tgz#b1587edd46476d271b3dd54c024054a964fa9f66" - integrity sha512-Amh7mRoDzZyJJ4efqoePqdoZOzharmSOttZuJDlVE5yy07BoE8hL6ZRpa5fNYn0LCqn/KoWs8OHANWxhKDGhvQ== - -"@nomicfoundation/edr-darwin-x64@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.23.tgz#6b61903e93eb40716ea3ed7c4f24f793e6566591" - integrity sha512-9wn489FIQm7m0UCD+HhktjWx6vskZzeZD9oDc2k9ZvbBzdXwPp5tiDqUBJ+eQpByAzCDfteAJwRn2lQCE0U+Iw== - -"@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.23.tgz#0c141a621c9dbe6b0dc414da5913b1f94833676e" - integrity sha512-nlk5EejSzEUfEngv0Jkhqq3/wINIfF2ED9wAofc22w/V1DV99ASh9l3/e/MIHOQFecIZ9MDqt0Em9/oDyB1Uew== - -"@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.23.tgz#bd996171aa0f90eb722984436449fd2ddb4de065" - integrity sha512-SJuPBp3Rc6vM92UtVTUxZQ/QlLhLfwTftt2XUiYohmGKB3RjGzpgduEFMCA0LEnucUckU6UHrJNFHiDm77C4PQ== - -"@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.23.tgz#fb411c5b5efeb96d0d859bb34ff0f466201c3f5d" - integrity sha512-NU+Qs3u7Qt6t3bJFdmmjd5CsvgI2bPPzO31KifM2Ez96/jsXYho5debtTQnimlb5NAqiHTSlxjh/F8ROcptmeQ== - -"@nomicfoundation/edr-linux-x64-musl@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.23.tgz#18685a6489167d1386db8d31823ce7d92fd789b5" - integrity sha512-F78fZA2h6/ssiCSZOovlgIu0dUeI7ItKPsDDF3UUlIibef052GCXmliMinC90jVPbrjUADMd1BUwjfI0Z8OllQ== - -"@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.23.tgz#20cc6fcdb22500df1e48fab0a397bc30a22ec37f" - integrity sha512-IfJZQJn7d/YyqhmguBIGoCKjE9dKjbu6V6iNEPApfwf5JyyjHYyyfkLU4rf7hygj57bfH4sl1jtQ6r8HnT62lw== - -"@nomicfoundation/edr@0.12.0-next.23": - version "0.12.0-next.23" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.12.0-next.23.tgz#483b29bed5165bf6f97b9be01f1536d0f1e1845a" - integrity sha512-F2/6HZh8Q9RsgkOIkRrckldbhPjIZY7d4mT9LYuW68miwGQ5l7CkAgcz9fRRiurA0+YJhtsbx/EyrD9DmX9BOw== - dependencies: - "@nomicfoundation/edr-darwin-arm64" "0.12.0-next.23" - "@nomicfoundation/edr-darwin-x64" "0.12.0-next.23" - "@nomicfoundation/edr-linux-arm64-gnu" "0.12.0-next.23" - "@nomicfoundation/edr-linux-arm64-musl" "0.12.0-next.23" - "@nomicfoundation/edr-linux-x64-gnu" "0.12.0-next.23" - "@nomicfoundation/edr-linux-x64-musl" "0.12.0-next.23" - "@nomicfoundation/edr-win32-x64-msvc" "0.12.0-next.23" - -"@nomicfoundation/hardhat-foundry@^1.1.2": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.2.1.tgz#2d0b8bd8d2815a4217d05381520520a6b74c2d3c" - integrity sha512-pH1KeyI0sysgi7I7uQKPLXWl895EkuS6V41rSi820Ipqp/FScIwDh27RbevgC9zJ4ufSsSz34njm9cvRMGMNVA== - dependencies: - picocolors "^1.1.0" - -"@nomicfoundation/hardhat-toolbox@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-5.0.0.tgz#165b47f8a3d2bf668cc5d453ce7f496a1156948d" - integrity sha512-FnUtUC5PsakCbwiVNsqlXVIWG5JIb5CEZoSXbJUsEBun22Bivx2jhF1/q9iQbzuaGpJKFQyOhemPB2+XlEE6pQ== - -"@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz#3a9c3b20d51360b20affb8f753e756d553d49557" - integrity sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw== - -"@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz#74dcfabeb4ca373d95bd0d13692f44fcef133c28" - integrity sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw== - -"@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz#4af5849a89e5a8f511acc04f28eb5d4460ba2b6a" - integrity sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA== - -"@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz#54036808a9a327b2ff84446c130a6687ee702a8e" - integrity sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA== - -"@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz#466cda0d6e43691986c944b909fc6dbb8cfc594e" - integrity sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g== - -"@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz#2b35826987a6e94444140ac92310baa088ee7f94" - integrity sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg== - -"@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz#e6363d13b8709ca66f330562337dbc01ce8bbbd9" - integrity sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA== - -"@nomicfoundation/solidity-analyzer@^0.1.0": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz#8bcea7d300157bf3a770a851d9f5c5e2db34ac55" - integrity sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA== - optionalDependencies: - "@nomicfoundation/solidity-analyzer-darwin-arm64" "0.1.2" - "@nomicfoundation/solidity-analyzer-darwin-x64" "0.1.2" - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu" "0.1.2" - "@nomicfoundation/solidity-analyzer-linux-arm64-musl" "0.1.2" - "@nomicfoundation/solidity-analyzer-linux-x64-gnu" "0.1.2" - "@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2" - "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2" - -"@scure/base@~1.1.0", "@scure/base@~1.1.6": - version "1.1.9" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" - integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== - -"@scure/base@~1.2.5": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" - integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== - -"@scure/bip32@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" - integrity sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw== - dependencies: - "@noble/hashes" "~1.2.0" - "@noble/secp256k1" "~1.7.0" - "@scure/base" "~1.1.0" - -"@scure/bip32@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" - integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== - dependencies: - "@noble/curves" "~1.4.0" - "@noble/hashes" "~1.4.0" - "@scure/base" "~1.1.6" - -"@scure/bip39@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" - integrity sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg== - dependencies: - "@noble/hashes" "~1.2.0" - "@scure/base" "~1.1.0" - -"@scure/bip39@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" - integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== - dependencies: - "@noble/hashes" "~1.4.0" - "@scure/base" "~1.1.6" - -"@sentry/core@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" - integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - -"@sentry/hub@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" - integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== - dependencies: - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - -"@sentry/minimal@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" - integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/types" "5.30.0" - tslib "^1.9.3" - -"@sentry/node@^5.18.1": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" - integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== - dependencies: - "@sentry/core" "5.30.0" - "@sentry/hub" "5.30.0" - "@sentry/tracing" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - -"@sentry/tracing@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" - integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - -"@sentry/types@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" - integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== - -"@sentry/utils@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" - integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== - dependencies: - "@sentry/types" "5.30.0" - tslib "^1.9.3" - -"@tsconfig/node10@^1.0.7": - version "1.0.12" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" - integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - -"@types/node@22.7.5": - version "22.7.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" - integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== - dependencies: - undici-types "~6.19.2" - -acorn-walk@^8.1.1: - version "8.3.5" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" - integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== - dependencies: - acorn "^8.11.0" - -acorn@^8.11.0, acorn@^8.4.1: - version "8.16.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" - integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== - -adm-zip@^0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== - -aes-js@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" - integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== - -aes-js@4.0.0-beta.5: - version "4.0.0-beta.5" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" - integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ansi-align@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - -ansi-colors@^4.1.1, ansi-colors@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - -ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -async-mutex@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.1.tgz#bccf55b96f2baf8df90ed798cb5544a1f6ee4c2c" - integrity sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA== - dependencies: - tslib "^2.4.0" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^1.16.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12" - integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A== - dependencies: - follow-redirects "^1.16.0" - form-data "^4.0.5" - https-proxy-agent "^5.0.1" - proxy-from-env "^2.1.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base-x@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.1.tgz#817fb7b57143c501f649805cb247617ad016a885" - integrity sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw== - -bech32@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" - integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bn.js@^4.11.9: - version "4.12.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.3.tgz#2cc2c679188eb35b006f2d0d4710bed8437a769e" - integrity sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g== - -bn.js@^5.2.1: - version "5.2.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" - integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== - -boxen@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" - integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^6.2.0" - chalk "^4.1.0" - cli-boxes "^2.2.1" - string-width "^4.2.2" - type-fest "^0.20.2" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - -brace-expansion@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.0.tgz#4f41a41190216ee36067ec381526fe9539c4f0ae" - integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w== - dependencies: - balanced-match "^1.0.0" - -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browser-stdout@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -bs58@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" - integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== - dependencies: - base-x "^4.0.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -bytes@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -camelcase@^6.0.0, camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== - dependencies: - readdirp "^4.0.1" - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -command-exists@^1.2.8: - version "1.2.9" - resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" - integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== - -commander@^8.1.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -cookie@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -debug@4, debug@^4.1.1, debug@^4.3.5: - version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -depd@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -diff@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" - integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== - -diff@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.2.tgz#0a4742797281d09cfa699b79ea32d27723623bad" - integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== - -dotenv@^16.0.0: - version "16.6.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" - integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -elliptic@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" - integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -enquirer@^2.3.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" - integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== - dependencies: - ansi-colors "^4.1.1" - strip-ansi "^6.0.1" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -escalade@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -ethereum-cryptography@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz#5ccfa183e85fdaf9f9b299a79430c044268c9b3a" - integrity sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw== - dependencies: - "@noble/hashes" "1.2.0" - "@noble/secp256k1" "1.7.1" - "@scure/bip32" "1.1.5" - "@scure/bip39" "1.1.1" - -ethereum-cryptography@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz#58f2810f8e020aecb97de8c8c76147600b0b8ccf" - integrity sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg== - dependencies: - "@noble/curves" "1.4.2" - "@noble/hashes" "1.4.0" - "@scure/bip32" "1.4.0" - "@scure/bip39" "1.3.0" - -ethers@^5.1.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.8.0.tgz#97858dc4d4c74afce83ea7562fe9493cedb4d377" - integrity sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg== - dependencies: - "@ethersproject/abi" "5.8.0" - "@ethersproject/abstract-provider" "5.8.0" - "@ethersproject/abstract-signer" "5.8.0" - "@ethersproject/address" "5.8.0" - "@ethersproject/base64" "5.8.0" - "@ethersproject/basex" "5.8.0" - "@ethersproject/bignumber" "5.8.0" - "@ethersproject/bytes" "5.8.0" - "@ethersproject/constants" "5.8.0" - "@ethersproject/contracts" "5.8.0" - "@ethersproject/hash" "5.8.0" - "@ethersproject/hdnode" "5.8.0" - "@ethersproject/json-wallets" "5.8.0" - "@ethersproject/keccak256" "5.8.0" - "@ethersproject/logger" "5.8.0" - "@ethersproject/networks" "5.8.0" - "@ethersproject/pbkdf2" "5.8.0" - "@ethersproject/properties" "5.8.0" - "@ethersproject/providers" "5.8.0" - "@ethersproject/random" "5.8.0" - "@ethersproject/rlp" "5.8.0" - "@ethersproject/sha2" "5.8.0" - "@ethersproject/signing-key" "5.8.0" - "@ethersproject/solidity" "5.8.0" - "@ethersproject/strings" "5.8.0" - "@ethersproject/transactions" "5.8.0" - "@ethersproject/units" "5.8.0" - "@ethersproject/wallet" "5.8.0" - "@ethersproject/web" "5.8.0" - "@ethersproject/wordlists" "5.8.0" - -ethers@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.16.0.tgz#fff9b4f05d7a359c774ad6e91085a800f7fccf65" - integrity sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A== - dependencies: - "@adraffy/ens-normalize" "1.10.1" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@types/node" "22.7.5" - aes-js "4.0.0-beta.5" - tslib "2.7.0" - ws "8.17.1" - -fdir@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" - integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -follow-redirects@^1.12.1, follow-redirects@^1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" - integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== - -form-data@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" - -fp-ts@1.19.3: - version "1.19.3" - resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f" - integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg== - -fp-ts@^1.0.0: - version "1.19.5" - resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.5.tgz#3da865e585dfa1fdfd51785417357ac50afc520a" - integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== - -fs-extra@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.1.2, graceful-fs@^4.1.6: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -hardhat@^2.22.7: - version "2.28.6" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.28.6.tgz#1346f90796492097ee6a802e762a2f4883817db6" - integrity sha512-zQze7qe+8ltwHvhX5NQ8sN1N37WWZGw8L63y+2XcPxGwAjc/SMF829z3NS6o1krX0sryhAsVBK/xrwUqlsot4Q== - dependencies: - "@ethereumjs/util" "^9.1.0" - "@ethersproject/abi" "^5.1.2" - "@nomicfoundation/edr" "0.12.0-next.23" - "@nomicfoundation/solidity-analyzer" "^0.1.0" - "@sentry/node" "^5.18.1" - adm-zip "^0.4.16" - aggregate-error "^3.0.0" - ansi-escapes "^4.3.0" - boxen "^5.1.2" - chokidar "^4.0.0" - ci-info "^2.0.0" - debug "^4.1.1" - enquirer "^2.3.0" - env-paths "^2.2.0" - ethereum-cryptography "^1.0.3" - find-up "^5.0.0" - fp-ts "1.19.3" - fs-extra "^7.0.1" - immutable "^4.0.0-rc.12" - io-ts "1.10.4" - json-stream-stringify "^3.1.4" - keccak "^3.0.2" - lodash "^4.17.11" - micro-eth-signer "^0.14.0" - mnemonist "^0.38.0" - mocha "^10.0.0" - p-map "^4.0.0" - picocolors "^1.1.0" - raw-body "^2.4.1" - resolve "1.17.0" - semver "^6.3.0" - solc "0.8.26" - source-map-support "^0.5.13" - stacktrace-parser "^0.1.10" - tinyglobby "^0.2.6" - tsort "0.0.1" - undici "^5.14.0" - uuid "^8.3.2" - ws "^7.4.6" - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasown@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c" - integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg== - dependencies: - function-bind "^1.1.2" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -http-errors@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" - integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== - dependencies: - depd "~2.0.0" - inherits "~2.0.4" - setprototypeof "~1.2.0" - statuses "~2.0.2" - toidentifier "~1.0.1" - -https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -iconv-lite@~0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -immutable@^4.0.0-rc.12: - version "4.3.8" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7" - integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -io-ts@1.10.4: - version "1.10.4" - resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2" - integrity sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g== - dependencies: - fp-ts "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -js-sha3@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - -js-yaml@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" - integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== - dependencies: - argparse "^2.0.1" - -json-stream-stringify@^3.1.4: - version "3.1.6" - resolved "https://registry.yarnpkg.com/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz#ebe32193876fb99d4ec9f612389a8d8e2b5d54d4" - integrity sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog== - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== - optionalDependencies: - graceful-fs "^4.1.6" - -keccak@^3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" - integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== - dependencies: - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - readable-stream "^3.6.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash@^4.17.11: - version "4.18.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" - integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== - -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -memorystream@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" - integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== - -micro-eth-signer@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz#8aa1fe997d98d6bdf42f2071cef7eb01a66ecb22" - integrity sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw== - dependencies: - "@noble/curves" "~1.8.1" - "@noble/hashes" "~1.7.1" - micro-packed "~0.7.2" - -micro-packed@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.7.3.tgz#59e96b139dffeda22705c7a041476f24cabb12b6" - integrity sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg== - dependencies: - "@scure/base" "~1.2.5" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@^5.0.1, minimatch@^5.1.6: - version "5.1.9" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" - integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== - dependencies: - brace-expansion "^2.0.1" - -mnemonist@^0.38.0: - version "0.38.5" - resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" - integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== - dependencies: - obliterator "^2.0.0" - -mocha@^10.0.0: - version "10.8.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" - integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== - dependencies: - ansi-colors "^4.1.3" - browser-stdout "^1.3.1" - chokidar "^3.5.3" - debug "^4.3.5" - diff "^5.2.0" - escape-string-regexp "^4.0.0" - find-up "^5.0.0" - glob "^8.1.0" - he "^1.2.0" - js-yaml "^4.1.0" - log-symbols "^4.1.0" - minimatch "^5.1.6" - ms "^2.1.3" - serialize-javascript "^6.0.2" - strip-json-comments "^3.1.1" - supports-color "^8.1.1" - workerpool "^6.5.1" - yargs "^16.2.0" - yargs-parser "^20.2.9" - yargs-unparser "^2.0.0" - -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -node-addon-api@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" - integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== - -node-gyp-build@^4.2.0: - version "4.8.4" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" - integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -obliterator@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.5.tgz#031e0145354b0c18840336ae51d41e7d6d2c76aa" - integrity sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -picocolors@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" - integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== - -picomatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" - integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== - -proxy-from-env@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" - integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -raw-body@^2.4.1: - version "2.5.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" - integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== - dependencies: - bytes "~3.1.2" - http-errors "~2.0.1" - iconv-lite "~0.4.24" - unpipe "~1.0.0" - -readable-stream@^3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -resolve@1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -scrypt-js@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" - integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== - -semver@^5.5.0: - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -semver@^6.3.0: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -setprototypeof@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -solc@0.8.26: - version "0.8.26" - resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" - integrity sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g== - dependencies: - command-exists "^1.2.8" - commander "^8.1.0" - follow-redirects "^1.12.1" - js-sha3 "0.8.0" - memorystream "^0.3.1" - semver "^5.5.0" - tmp "0.0.33" - -source-map-support@^0.5.13: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -stacktrace-parser@^0.1.10: - version "0.1.11" - resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz#c7c08f9b29ef566b9a6f7b255d7db572f66fabc4" - integrity sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg== - dependencies: - type-fest "^0.7.1" - -statuses@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" - integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -tiny-invariant@^1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" - integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== - -tinyglobby@^0.2.6: - version "0.2.16" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" - integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== - dependencies: - fdir "^6.5.0" - picomatch "^4.0.4" - -tmp@0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -ts-node@^10.9.0: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tslib@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== - -tslib@^1.9.3: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.4.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -tsort@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" - integrity sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - -typescript@^5.0.0: - version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== - -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - -undici@^5.14.0: - version "5.29.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" - integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg== - dependencies: - "@fastify/busboy" "^2.0.0" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - -workerpool@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" - integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@8.17.1: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -ws@8.18.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - -ws@^7.4.6: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yargs-parser@^20.2.2, yargs-parser@^20.2.9: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-unparser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From b169b66abaafa757ee7e2f473e6855ecf3d0cc81 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 01:53:15 +0530 Subject: [PATCH 27/69] feat: support direct deposit via arbitrum native bridge --- scripts/e2e/config.ts | 1 + scripts/e2e/swapBridgeViaArbitrumNative.ts | 455 +++++++++++---------- 2 files changed, 247 insertions(+), 209 deletions(-) diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 164c6a6..0c39f70 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -37,6 +37,7 @@ export const ROUTER_BY_CHAIN_ID: Record = { [CHAIN_IDS.POLYGON]: '0x23D5aFEF7cE44257366D9ef6de80Ea334FAa9d25', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', [CHAIN_IDS.BASE]: '0x96E8c261fCCDFca2CCffe8b4A33dC8a65f153785', + [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', }; const ADDR_HEX_RE = /^0x[a-fA-F0-9]{40}$/; diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index b22647d..767d0c0 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -1,37 +1,40 @@ /** - * Script 3 — Swap AAVE→ETH on Ethereum, then bridge ETH to Arbitrum via - * the Arbitrum native inbox (depositEth) + * Arbitrum bridge e2e script — AAVE (Ethereum) → ETH (OO swap) → Arbitrum ETH (depositEth) * * Flow: - * 1. Fetch an OpenOcean swap quote for AAVE→ETH on Ethereum mainnet. - * 2. Estimate the Arbitrum retryable submission fee using @arbitrum/sdk so we - * know the minimum ETH required to bridge. A conservative fallback of - * 0.001 ETH is used if estimation fails. - * 3. Build a post-swap fee to signer in ETH. - * 4. Build either monolithic or modular execution payload. - * - Monolithic: swap AAVE→ETH (decoded return amount), take ETH fee, - * call Arbitrum inbox with useFinalAmountAsValue=true so finalAmount - * becomes msg.value on the depositEth call. - * - Modular: pull → approve(oo) → swap(oo) → send ETH fee via CALL_WITH_NATIVE → - * depositEth via CALL_WITH_NATIVE. - * 5. Call AllowanceHolder.exec with msg.value=0 (AAVE is the input token, not ETH). + * 1. Fetch an OpenOcean swap quote for AAVE → ETH on Ethereum mainnet (router is sender). + * 2. Estimate the Arbitrum retryable submission fee so we know the minimum ETH required + * to bridge. A conservative fallback of 0.001 ETH is used if estimation fails. + * 3. Split the signer's AAVE balance in half and run two legs back-to-back: + * Leg 1 MONOLITHIC — single `performExecution` call + * Leg 2 MODULAR — `performModularExecution` call (3-second pause before) * - * Uses the signer’s full AAVE balance on Ethereum mainnet as swap input. + * Monolithic mechanics: + * - Pull inputAmount AAVE via AH.exec grant, approve OO router, swap AAVE → ETH. + * - Post-swap fee (FEE_BPS) in ETH sent to signer. + * - useFinalAmountAsValue=true: router forwards actualFinalETH as msg.value to inbox. + * - No ETH splice needed (depositEth takes no calldata amount param). + * + * Modular mechanics: + * [0] AH.transferFrom AAVE → router (uses ephemeral AH grant) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] OO swap AAVE → ETH (lands in router) + * [3] nativeCall(signer, '0x', feeAmount) — ETH fee out + * [4] nativeCall(inbox, depositEth(), bridgeValue) + * + * Input is always AAVE (ERC-20) so `direct` router txs are rejected — the router's + * `_pullFromUser` requires the ephemeral allowance set by AllowanceHolder.exec. + * + * Exec mode (argv[1] or ARB_ROUTER_EXEC env): + * allowance-holder (default) — wrap via AllowanceHolder.exec + * direct — rejected for ERC-20 input with a clear error * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts - * USE_MODULAR=true PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts - * - * Router on Ethereum mainnet: set `ROUTER_CHAIN_1` or legacy `ROUTER_ADDRESS` - * ({@link ROUTER_BY_CHAIN_ID} has no Ethereum entry). + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts allowance-holder + * ARB_ROUTER_EXEC=allowance-holder PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts * - * Notes: - * - The router must retain enough ETH after the swap to cover both the fee - * and the Arbitrum retryable submission cost. The script warns if the - * estimated ETH output is insufficient. - * - The Arbitrum Delayed Inbox address is 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f - * on Ethereum mainnet. depositEth() accepts ETH as msg.value and credits - * the sender's L2 address on Arbitrum. + * Router on Ethereum mainnet: set `ROUTER_CHAIN_1` env or legacy `ROUTER_ADDRESS`. */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -50,96 +53,119 @@ import { ALLOWANCE_HOLDER, NATIVE_TOKEN_ADDRESS, } from './config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; +import { + execViaAH, + execDirect, + ensureAllowanceForAllowanceHolder, +} from './utils/allowanceHolder'; import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - MonolithicExecution, - NO_FEE, - ZERO_ADDRESS, -} from './utils/contractTypes'; +import { MonolithicExecution, NO_FEE, ZERO_ADDRESS } from './utils/contractTypes'; +import { sleep } from './utils/sleep'; +import { logTxnSummary } from './utils/txnLogSummary'; + +// ─── Exec-mode selection ────────────────────────────────────────────────────── -const ROUTER_ETHEREUM = routerAddressForChain(CHAIN_IDS.ETHEREUM); +/** How the signer reaches the router. */ +type RouterExecRoute = 'direct' | 'allowance-holder'; -// ─── Arbitrum retryable fee estimation ─────────────────────────────────────── +const EXEC_ALIASES: Record = { + direct: 'direct', + dr: 'direct', + router: 'direct', + 'allowance-holder': 'allowance-holder', + ah: 'allowance-holder', + exec: 'allowance-holder', +}; + +/** + * Resolves exec route from `argv[1]` (overrides) or `ARB_ROUTER_EXEC` env. + * Defaults to `allowance-holder` since AAVE is ERC-20. + * `direct` is rejected with a clear error because `_pullFromUser` requires AH. + */ +function resolveRouterExecRoute(): RouterExecRoute { + const rawArg = typeof process.argv[2] === 'string' ? process.argv[2].trim().toLowerCase() : ''; + const rawEnv = (process.env.ARB_ROUTER_EXEC ?? '').trim().toLowerCase(); + const raw = rawArg || rawEnv; + + if (raw) { + const route = EXEC_ALIASES[raw]; + if (!route) { + console.error( + `Unknown exec mode "${raw}". Use argv[1] or ARB_ROUTER_EXEC: allowance-holder | direct (aliases ah, exec, dr, router).`, + ); + process.exit(1); + } + if (route === 'direct') { + console.error( + 'ERC-20 input (AAVE) cannot use direct router txs: `_pullFromUser` invokes AllowanceHolder.transferFrom, ' + + 'which requires the ephemeral allowance set by AH.exec. Use allowance-holder (default).', + ); + process.exit(1); + } + return route; + } + + return 'allowance-holder'; +} + +// ─── Arbitrum bridge fee estimation ────────────────────────────────────────── /** * Estimates the minimum ETH required for the Arbitrum inbox submission fee. - * Uses @arbitrum/sdk's ParentToChildMessageGasEstimator if available. - * Falls back to a conservative hardcoded estimate (0.001 ETH) so the script - * can run without a live Arbitrum RPC for fee estimation. - * - * For a depositEth(), the inbox contract only needs the submission fee; there - * is no retryable gas limit to estimate (it's a direct ETH credit on L2). + * Falls back to 0.001 ETH if the SDK is unavailable or estimation fails. */ -async function estimateArbitrumBridgeFee( - ethereumProvider: ethers.Provider, -): Promise { +async function estimateArbitrumBridgeFee(ethereumProvider: ethers.Provider): Promise { try { - // @arbitrum/sdk types vary between versions; dynamic import avoids hard-dep issues. // eslint-disable-next-line @typescript-eslint/no-var-requires const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); const estimator = new ParentToChildMessageGasEstimator(ethereumProvider); - // Estimate submission fee for a minimal retryable (0 calldata, 250k gas limit). - const l2GasPrice = - (await ( - await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData() - ).gasPrice) ?? 0n; - const submissionFee = await estimator.estimateSubmissionFee( - ethereumProvider, - 0n, // l1BaseFee (fetched internally) - 0n, // callDataLength - ); - // Add buffer: submission fee + retryable execution cost headroom - const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); // gasPrice * 1.2 + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(ethereumProvider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); const totalFee = BigInt(submissionFee.toString()) + executionCost; - console.log( - `Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`, - ); + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); return totalFee; } catch (err) { - // Fallback: 0.001 ETH is a safe overestimate for L1→L2 ETH deposits in 2024-2026. const fallback = ethers.parseEther('0.001'); console.warn( - `Could not estimate Arbitrum fee via SDK (${ - (err as Error).message - }), using fallback: ${ethers.formatEther(fallback)} ETH`, + ` Arbitrum fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`, ); return fallback; } } -// ─── OpenOcean swap quote ───────────────────────────────────────────────────── +// ─── OpenOcean quote ────────────────────────────────────────────────────────── -interface OpenOceanSwapQuoteResponse { +interface OoSwapQuoteResponse { data: { to: string; data: string; - value: string; + value?: string; outAmount: string; minOutAmount: string; }; } /** - * Fetches an OpenOcean swap quote for AAVE→ETH on Ethereum mainnet. - * The native ETH output address used by OpenOcean is 0xEeee...EEe. + * Fetches an OpenOcean swap_quote for AAVE → ETH on Ethereum mainnet. + * Router address is used as sender and account so ETH output lands in the router. */ -async function fetchOpenOceanSwapQuote( +async function fetchOoQuote( routerAddress: string, inputAmount: bigint, slippageBps: number = 100, ): Promise<{ - ooRouterAddress: string; + ooRouter: string; swapData: string; - minAmountOut: bigint; estimatedOut: bigint; + minAmountOut: bigint; }> { const params: Record = { inTokenAddress: TOKENS.AAVE_ETH, - outTokenAddress: NATIVE_TOKEN_ADDRESS, // ETH output + outTokenAddress: NATIVE_TOKEN_ADDRESS, amount: ethers.formatUnits(inputAmount, 18), slippage: (slippageBps / 100).toString(), sender: routerAddress, @@ -147,80 +173,64 @@ async function fetchOpenOceanSwapQuote( gasPrice: '20', }; if (OPEN_OCEAN_API_KEY) { - params['apikey'] = OPEN_OCEAN_API_KEY; + params.apikey = OPEN_OCEAN_API_KEY; } - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; - const response = await axios.get(url, { params }); + const response = await axios.get(url, { params }); const q = response.data.data; - return { - ooRouterAddress: q.to, + ooRouter: q.to, swapData: q.data, - minAmountOut: BigInt(q.minOutAmount), estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), }; } -// ─── Arbitrum inbox calldata ────────────────────────────────────────────────── +// ─── Calldata helpers ───────────────────────────────────────────────────────── -/** - * Builds the calldata for Arbitrum inbox depositEth(). - * The ETH amount is entirely determined by msg.value — there is no amount - * parameter in the calldata itself. - */ +/** Encodes Arbitrum inbox `depositEth()` — ETH amount is entirely in msg.value. */ function buildDepositEthCalldata(): string { - const iface = new ethers.Interface([ + return new ethers.Interface([ 'function depositEth() external payable returns (uint256)', - ]); - return iface.encodeFunctionData('depositEth', []); + ]).encodeFunctionData('depositEth', []); } // ─── Monolithic builder ─────────────────────────────────────────────────────── /** - * Builds a MonolithicExecution that: - * - Pulls inputAmount AAVE from user - * - Swaps AAVE → ETH via OpenOcean (decoded return amount) - * - Takes feeAmount ETH as post-swap fee sent to signer - * - Calls Arbitrum inbox depositEth() with finalAmount as msg.value - * (via useFinalAmountAsValue=true — no amount to splice in calldata) + * AAVE → OO → ETH → Arbitrum inbox (monolithic): + * - input: AAVE pulled via AH + * - swap: AAVE → native ETH, useFinalAmountAsValue=true forwards actualFinalETH + * - bridge: depositEth() — no amount in calldata, all ETH passed as msg.value */ -function buildMonolithicExecution( +function buildMonolithic( signerAddress: string, inputAmount: bigint, feeAmount: bigint, minAmountOut: bigint, - ooRouterAddress: string, + ooRouter: string, swapData: string, ): MonolithicExecution { return { - input: { - user: signerAddress, - inputToken: TOKENS.AAVE_ETH, - inputAmount, - }, + input: { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, preFee: NO_FEE, swap: { - target: ooRouterAddress, - approvalSpender: ooRouterAddress, + target: ooRouter, + approvalSpender: ooRouter, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, data: swapData, returnDataWordOffset: 0n, }, - postFee: { - receiver: signerAddress, - amount: feeAmount, - }, + postFee: { receiver: signerAddress, amount: feeAmount }, bridge: { target: ARBITRUM_INBOX, - approvalSpender: ZERO_ADDRESS, // no ERC-20 approval needed for native - value: 0n, // ignored when useFinalAmountAsValue=true + approvalSpender: ZERO_ADDRESS, + value: 0n, // ignored — useFinalAmountAsValue=true data: buildDepositEthCalldata(), - amountPositions: [], // ETH goes as msg.value, not in calldata - useFinalAmountAsValue: true, // forward finalAmount as msg.value to inbox + amountPositions: [], // no amount in calldata + useFinalAmountAsValue: true, }, }; } @@ -228,12 +238,12 @@ function buildMonolithicExecution( // ─── Modular builder ────────────────────────────────────────────────────────── /** - * Builds an Action array: - * [0] Pull AAVE via AH.transferFrom - * [1] Approve OpenOcean router for inputAmount - * [2] Call OpenOcean to swap AAVE → ETH (lands in router as ETH) - * [3] Send ETH fee to signer via CALL_WITH_NATIVE - * [4] Call Arbitrum inbox depositEth() via CALL_WITH_NATIVE + * AAVE → OO → ETH → Arbitrum inbox (modular): + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — AAVE → ETH, ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) + * [4] nativeCall(inbox, depositEthData, bridgeValue) */ function buildModularActions( signerAddress: string, @@ -241,138 +251,165 @@ function buildModularActions( inputAmount: bigint, feeAmount: bigint, bridgeValue: bigint, - ooRouterAddress: string, + ooRouter: string, swapData: string, ): ModularAction[] { const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.AAVE_ETH, - signerAddress, - routerAddress, - inputAmount, - ]); - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouterAddress, inputAmount)); - exec.call(ooRouterAddress, swapData); + + exec.call( + ALLOWANCE_HOLDER, + ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_ETH, signerAddress, routerAddress, inputAmount]), + ); + exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); + return exec.toActions(); } -// ─── Main ───────────────────────────────────────────────────────────────────── +// ─── Execution leg ──────────────────────────────────────────────────────────── -async function main() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.AAVE_ETH; - const { balance: inputAmount, decimals: inputDecimals } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (inputAmount === 0n) { - throw new Error( - `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with AAVE on Ethereum first.`, - ); - } - const useModular = true; +/** + * Runs one monolithic or modular leg: fetches OO quote + arb fee, builds calldata, + * dispatches via AllowanceHolder.exec (msg.value=0 since input is AAVE). + */ +async function executeLeg( + legLabel: string, + useModular: boolean, + routerAddress: string, + signer: ethers.Wallet, + signerAddress: string, + provider: ethers.JsonRpcProvider, + inputAmount: bigint, + routerExec: RouterExecRoute, + routerIface: ethers.Interface, +): Promise { + console.log(`\n── ${legLabel} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ETHEREUM}`); - console.log(`Input token: ${inputToken}`); - console.log( - `Input: ${ethers.formatUnits(inputAmount, inputDecimals)} (full wallet balance)`, + console.log('Fetching OpenOcean quote (AAVE → ETH)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOoQuote( + routerAddress, + inputAmount, ); - console.log(`Mode: ${useModular ? 'MODULAR' : 'MONOLITHIC'}`); - console.log(''); - - // Fetch OpenOcean quote (AAVE → ETH on Ethereum) - console.log('Fetching OpenOcean swap quote (AAVE→ETH Ethereum)...'); - const { ooRouterAddress, swapData, minAmountOut, estimatedOut } = - await fetchOpenOceanSwapQuote(ROUTER_ETHEREUM, inputAmount); - const feeAmount = bpsOf(estimatedOut, FEE_BPS); - console.log(`OO Router: ${ooRouterAddress}`); - console.log(`Est. ETH out: ${ethers.formatEther(estimatedOut)} ETH`); - console.log( - `Post-swap fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`, - ); - console.log(`Min ETH out: ${ethers.formatEther(minAmountOut)} ETH`); - // Estimate Arbitrum bridge fee + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH out: ${ethers.formatEther(estimatedOut)} ETH`); + console.log(` Fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH out: ${ethers.formatEther(minAmountOut)} ETH`); + const arbFee = await estimateArbitrumBridgeFee(provider); const minEthRequired = feeAmount + arbFee; if (estimatedOut < minEthRequired) { console.warn( - `Warning: estimated ETH output (${ethers.formatEther( - estimatedOut, - )}) may be insufficient ` + - `to cover fee + bridge cost (${ethers.formatEther( - minEthRequired, - )}). Increase AAVE balance on Ethereum so the quoted swap output rises.`, + ` Warning: est. ETH out (${ethers.formatEther(estimatedOut)}) may be insufficient ` + + `to cover fee + bridge cost (${ethers.formatEther(minEthRequired)}).`, ); } - console.log(''); - const routerIface = new ethers.Interface(ROUTER_ABI); - let execCalldata: string; + // bridgeValue = everything left after the fee; use minAmountOut-based floor so + // the modular nativeCall carries at least as much ETH as the inbox requires. + const bridgeValue = minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n; + console.log(` Bridge value: ${ethers.formatEther(bridgeValue)} ETH (floor for nativeCall)`); + let execCalldata: string; if (useModular) { const actions = buildModularActions( signerAddress, - ROUTER_ETHEREUM, + routerAddress, inputAmount, feeAmount, - minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n, - ooRouterAddress, + bridgeValue, + ooRouter, swapData, ); - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - actions, - ]); - console.log('Using performModularExecution'); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); } else { - const exec = buildMonolithicExecution( - signerAddress, + const mono = buildMonolithic(signerAddress, inputAmount, feeAmount, minAmountOut, ooRouter, swapData); + execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); + } + + // Input is AAVE (ERC-20) — msg.value is always 0; ETH comes from the swap output. + const txValue = 0n; + + let receipt: ethers.TransactionReceipt; + if (routerExec === 'direct') { + // Guarded at startup — should never reach here for ERC-20 input. + console.log(`[exec=direct] value=0 ETH`); + receipt = await execDirect(signer, routerAddress, execCalldata, txValue); + } else { + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); + console.log(`[exec=allowance-holder] value=0 ETH`); + receipt = await execViaAH( + signer, + routerAddress, + TOKENS.AAVE_ETH, inputAmount, - feeAmount, - minAmountOut, - ooRouterAddress, - swapData, + routerAddress, + execCalldata, + txValue, ); - execCalldata = routerIface.encodeFunctionData('performExecution', [exec]); - console.log('Using performExecution (monolithic)'); } - // AH.exec is called with AAVE as the token grant — ETH is handled internally - // by the swap. msg.value=0 since the input token is ERC-20. - await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - console.log('Sending AllowanceHolder.exec transaction...'); - const receipt = await execViaAH( - signer, - ROUTER_ETHEREUM, - TOKENS.AAVE_ETH, - inputAmount, - ROUTER_ETHEREUM, - execCalldata, - 0n, // no ETH needed from caller; ETH comes from the swap output + logTxnSummary( + `AAVE → ETH → Arbitrum — ${useModular ? 'Modular' : 'Monolithic'}`, + CHAIN_IDS.ETHEREUM, + receipt, ); +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const routerExec = resolveRouterExecRoute(); + const routerAddress = routerAddressForChain(CHAIN_IDS.ETHEREUM); - console.log(`\nSuccess! Gas used: ${receipt.gasUsed.toString()}`); - console.log( - `ETH will arrive on Arbitrum at ${signerAddress} (via inbox deposit).`, + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: fullBalance, decimals } = await getWalletErc20Balance( + TOKENS.AAVE_ETH, + signerAddress, + provider, ); + if (fullBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero AAVE on Ethereum. Fund the wallet first.`, + ); + } + + const legAmount = fullBalance / 2n; + if (legAmount === 0n) { + throw new Error('AAVE balance too small to split into two legs.'); + } + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${routerAddress}`); + console.log(`Input token: ${TOKENS.AAVE_ETH} (AAVE Ethereum)`); + console.log(`Balance: ${ethers.formatUnits(fullBalance, decimals)} AAVE`); + console.log(`Per leg (½): ${ethers.formatUnits(legAmount, decimals)} AAVE`); + console.log(`Exec route: ${routerExec}`); + + await executeLeg('1/2', false, routerAddress, signer, signerAddress, provider, legAmount, routerExec, routerIface); + + console.log('\nSleeping 3s before modular leg...'); + await sleep(3000); + + await executeLeg('2/2', true, routerAddress, signer, signerAddress, provider, legAmount, routerExec, routerIface); + + console.log('\n✓ Arbitrum bridge case completed.'); } main().catch((err) => { From c7d0c1c4d48dd1ba9ed2337b0e3944c4c2258766 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 17:10:07 +0530 Subject: [PATCH 28/69] feat: simple bridge function --- src/combined/BungeeOpenRouterV2Unchecked.sol | 76 ++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 33674ea..a836eb2 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -69,6 +69,16 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { bool useFinalAmountAsValue; } + /// @dev Simplified bridge descriptor for the no-swap `bridge()` path. + /// The caller knows `finalAmount = inputAmount - feeAmount` before encoding, + /// so no amount-splicing or runtime value overrides are needed. + struct StaticBridgeData { + address target; + address approvalSpender; + uint256 value; + bytes data; + } + struct MonolithicExecution { InputData input; FeeData preFee; @@ -132,6 +142,72 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { _runMonolithic(exec); } + // ========================================================================= + // External: simple bridge path (no swap) + // ========================================================================= + + /** + * @notice Pull → optional pre-bridge fee → bridge, with no swap step. + * @dev `feeBytes` must be either empty (`0x`, skip fee) or exactly 64 bytes + * ABI-encoded as `(address receiver, uint256 amount)`. Any other + * length reverts with `InvalidExecution`. + * + * Because no swap is involved, `finalAmount = inputAmount - feeAmount` is + * fully knowable by the caller before signing. The caller must therefore + * bake the correct amount directly into `bridgeData.data` and set + * `bridgeData.value` to the desired `msg.value` for the bridge call. + * No runtime calldata splicing is performed. + * + * The caller MUST route through `AllowanceHolder.exec` for ERC-20 + * inputs so that `_msgSender()` resolves to `input.user`. + */ + function bridge(InputData calldata input, bytes calldata feeBytes, StaticBridgeData calldata bridgeData) + external + payable + { + if (bridgeData.target == address(0) || input.user == address(0) || input.inputToken == address(0)) { + revert InvalidExecution(); + } + + // feeBytes must be empty or exactly one ABI word-pair (address + uint256 = 64 bytes) + if (feeBytes.length != 0 && feeBytes.length != 64) { + revert InvalidExecution(); + } + + // 1. pull funds from user via AllowanceHolder + _pullFromUser(input.inputToken, input.user, input.inputAmount); + + // 2. optional pre-bridge fee decoded from feeBytes; track net amount for approval + uint256 feeAmount; + if (feeBytes.length == 64) { + address feeReceiver; + assembly ("memory-safe") { + // feeBytes is a calldata slice: feeBytes.offset points at the raw bytes + feeReceiver := calldataload(feeBytes.offset) + feeAmount := calldataload(add(feeBytes.offset, 0x20)) + } + if (feeAmount != 0) { + if (feeAmount > input.inputAmount) { + revert InsufficientFunds(); + } + CurrencyLib.transfer(input.inputToken, feeReceiver, feeAmount); + } + } + + // 3. optional approval to bridge spender for the net amount (inputAmount - feeAmount) + if (bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + uint256 netAmount; + unchecked { + netAmount = input.inputAmount - feeAmount; + } + SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, netAmount); + } + + // 4. bridge call — data and value are pre-encoded by the caller + bytes memory bData = bridgeData.data; + _doCall(bridgeData.target, bridgeData.value, bData, false); + } + // ========================================================================= // External: modular path // ========================================================================= From e85dba5b2edb8c820a688f342d75fb656ff286c4 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 17:10:24 +0530 Subject: [PATCH 29/69] feat: simple bridge relay test --- scripts/deploy/deployBungeeOpenRouterV2.ts | 8 +- scripts/e2e/bridgeViaRelaySimple.ts | 240 +++++++++++++++++++++ scripts/e2e/config.ts | 2 +- scripts/e2e/utils/contractTypes.ts | 8 + scripts/e2e/utils/routerAbi.ts | 7 + 5 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 scripts/e2e/bridgeViaRelaySimple.ts diff --git a/scripts/deploy/deployBungeeOpenRouterV2.ts b/scripts/deploy/deployBungeeOpenRouterV2.ts index 5d51a2e..3d69756 100644 --- a/scripts/deploy/deployBungeeOpenRouterV2.ts +++ b/scripts/deploy/deployBungeeOpenRouterV2.ts @@ -13,10 +13,12 @@ * Omitting --network runs against the in-process Hardhat network. */ +import hre from 'hardhat'; import { ethers } from 'hardhat'; async function main() { const [deployer] = await ethers.getSigners(); + const networkName = hre.network.name; const owner = deployer.address; const openRouterSigner = deployer.address; @@ -28,7 +30,7 @@ async function main() { console.log('Deployer: ', deployer.address); console.log('Owner: ', owner); console.log('OpenRouterSigner: ', openRouterSigner); - console.log('Network: ', (await ethers.provider.getNetwork()).name); + console.log('Network: ', networkName); console.log(''); // ------------------------------------------------------------------------- @@ -67,10 +69,10 @@ async function main() { if (chainId !== 31337n) { console.log('\nTo verify on a block explorer:'); // console.log( - // ` npx hardhat verify --network ${v2Address} "${owner}" "${openRouterSigner}"` + // ` npx hardhat verify --network ${networkName} ${v2Address} "${owner}" "${openRouterSigner}"` // ); console.log( - ` npx hardhat verify --network ${v2uAddress} "${owner}"`, + ` npx hardhat verify --network ${networkName} ${v2uAddress} "${owner}"`, ); } } diff --git a/scripts/e2e/bridgeViaRelaySimple.ts b/scripts/e2e/bridgeViaRelaySimple.ts new file mode 100644 index 0000000..d88ce24 --- /dev/null +++ b/scripts/e2e/bridgeViaRelaySimple.ts @@ -0,0 +1,240 @@ +/** + * Script — Bridge AAVE (Polygon PoS) → AAVE (Base) via Relay.link + * using the `bridge(InputData, bytes feeBytes, BridgeData)` entrypoint. + * + * Flow: + * 1. Read signer's Polygon AAVE (or USDC) balance. + * 2. Compute fee via FEE_BPS; encode as 64-byte ABI word-pair (receiver, amount). + * If FEE_AMOUNT_BPS=0, feeBytes is `0x` and the contract skips the fee entirely. + * 3. Fetch Relay.link /quote/v2 for the net bridge amount (inputAmount − fee). + * 4. AllowanceHolder.exec → router.bridge(input, feeBytes, bridgeData). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts + * + * USDC path (Polygon Circle USDC → Base USDC): + * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts usdc + * + * No fee: + * FEE_AMOUNT_BPS=0 PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts + * + * Router addresses: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain` in config.ts. + * Override with `ROUTER_CHAIN_137` or legacy `ROUTER_ADDRESS` if needed. + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from './config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; +import { getWalletErc20Balance } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import type { InputData, StaticBridgeData } from './utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; +import { logTxnSummary } from './utils/txnLogSummary'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +// ─── feeBytes encoding ──────────────────────────────────────────────────────── + +/** + * Encode fee as the 64-byte ABI word-pair expected by the contract: + * abi.encode(address receiver, uint256 amount) + * Returns `'0x'` when feeAmount is zero so the contract skips the transfer. + */ +function encodeFeeBytes(receiver: string, feeAmount: bigint): string { + if (feeAmount === 0n) { + return '0x'; + } + return ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256'], + [receiver, feeAmount], + ); +} + +// ─── Execution builder ──────────────────────────────────────────────────────── + +interface BridgeParams { + signerAddress: string; + inputToken: string; + inputAmount: bigint; + feeBytes: string; + relaySpender: string; + depositTarget: string; + /** Bridge calldata with finalAmount (= inputAmount - feeAmount) already encoded inside. */ + depositData: string; +} + +function buildBridgeCalldata(routerIface: ethers.Interface, p: BridgeParams): string { + const input: InputData = { + user: p.signerAddress, + inputToken: p.inputToken, + inputAmount: p.inputAmount, + }; + + // No amountPositions or useFinalAmountAsValue — the caller bakes the net amount + // directly into depositData before calling. + const bridgeData: StaticBridgeData = { + target: p.depositTarget, + approvalSpender: p.relaySpender, + value: 0n, + data: p.depositData, + }; + + return routerIface.encodeFunctionData('bridge', [input, p.feeBytes, bridgeData]); +} + +// ─── Execution leg ──────────────────────────────────────────────────────────── + +interface LegConfig { + label: string; + inputToken: string; + decimals: number; + symbol: string; + originChainId: number; + destinationChainId: number; + destinationCurrency: string; +} + +async function executeLeg(args: { + config: LegConfig; + signer: ethers.Wallet; + signerAddress: string; + inputAmount: bigint; + routerIface: ethers.Interface; +}): Promise { + const { config, signer, signerAddress, inputAmount, routerIface } = args; + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + const fmt = (n: bigint) => ethers.formatUnits(n, config.decimals); + + console.log(`\n── ${config.label} ──`); + console.log(`Input: ${fmt(inputAmount)} ${config.symbol}`); + console.log(`Fee (${FEE_BPS} bps): ${fmt(feeAmount)} ${config.symbol}`); + console.log(`Bridge amount: ${fmt(bridgeAmount)} ${config.symbol}`); + + // feeBytes: `0x` if no fee, else abi-encoded (receiver=signer, amount) + const feeBytes = encodeFeeBytes(signerAddress, feeAmount); + console.log(`feeBytes: ${feeBytes === '0x' ? '0x (no fee)' : `${feeBytes.slice(0, 18)}… (${feeBytes.length / 2 - 1} bytes)`}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: config.originChainId, + destinationChainId: config.destinationChainId, + originCurrency: config.inputToken, + destinationCurrency: config.destinationCurrency, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + const execCalldata = buildBridgeCalldata(routerIface, { + signerAddress, + inputToken: config.inputToken, + inputAmount, + feeBytes, + relaySpender, + depositTarget, + depositData, + }); + + await ensureAllowanceForAllowanceHolder(signer, config.inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + config.inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + ); + + logTxnSummary(config.label, config.originChainId, receipt); +} + +// ─── Entry points ───────────────────────────────────────────────────────────── + +async function run(useUsdc: boolean): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const legConfig: LegConfig = useUsdc + ? { + label: 'Polygon USDC → Base USDC — Relay — Simple Bridge', + inputToken: TOKENS.USDC_POLYGON_CIRCLE, + decimals: 6, + symbol: 'USDC', + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + destinationCurrency: TOKENS.USDC_BASE, + } + : { + label: 'Polygon AAVE → Base AAVE — Relay — Simple Bridge', + inputToken: TOKENS.AAVE_POLYGON, + decimals: 18, + symbol: 'AAVE', + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + destinationCurrency: TOKENS.AAVE_BASE, + }; + + const { balance: walletBalance } = await getWalletErc20Balance( + legConfig.inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero balance of ${legConfig.inputToken}. Fund the wallet first.`, + ); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Input token: ${legConfig.inputToken}`); + console.log( + `Balance: ${ethers.formatUnits(walletBalance, legConfig.decimals)} ${legConfig.symbol}`, + ); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + await executeLeg({ + config: legConfig, + signer, + signerAddress, + inputAmount: walletBalance, + routerIface, + }); + + console.log('\nDone.'); +} + +async function main(): Promise { + const arg = process.argv[2]?.toLowerCase(); + const useUsdc = arg === 'usdc' || arg === 'usdc-polygon-base'; + await run(useUsdc); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 0c39f70..c14576d 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -34,7 +34,7 @@ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. */ export const ROUTER_BY_CHAIN_ID: Record = { - [CHAIN_IDS.POLYGON]: '0x23D5aFEF7cE44257366D9ef6de80Ea334FAa9d25', + [CHAIN_IDS.POLYGON]: '0x5bfbF2d49658e48D209449B3E263DC6F774B6E6f', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', [CHAIN_IDS.BASE]: '0x96E8c261fCCDFca2CCffe8b4A33dC8a65f153785', [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index aeaf23e..ff9b5b6 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -36,6 +36,14 @@ export interface BridgeData { useFinalAmountAsValue: boolean; } +/** Simplified bridge descriptor for the no-swap `bridge()` entrypoint. */ +export interface StaticBridgeData { + target: string; + approvalSpender: string; + value: bigint; + data: string; +} + export interface MonolithicExecution { input: InputData; preFee: FeeData; diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index a64499f..f2dae81 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -18,4 +18,11 @@ export const ROUTER_ABI = [ `function performModularExecution( (uint256 actionInfo, bytes data, uint256[] splices)[] actions ) external payable`, + + // Simple bridge path (no swap, no splicing — caller pre-encodes finalAmount into data) + `function bridge( + (address user, address inputToken, uint256 inputAmount) input, + bytes feeBytes, + (address target, address approvalSpender, uint256 value, bytes data) bridgeData + ) external payable`, ] as const; From e8e1074601c0941044f3592407054e68129082e5 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 19:11:48 +0530 Subject: [PATCH 30/69] feat: approval, balance dust during gas tests --- scripts/e2e/approveViaModular.ts | 134 ++++++++++++++ scripts/e2e/bridgeViaRelay.ts | 14 +- scripts/e2e/bridgeViaRelaySimple.ts | 9 +- scripts/e2e/swapBridgeViaArbitrumNative.ts | 11 +- scripts/e2e/swapBridgeViaCctp.ts | 16 +- scripts/e2e/swapBridgeViaCctpSimple.ts | 199 +++++++++++++++++++++ scripts/e2e/swapBridgeViaOft.ts | 16 +- scripts/e2e/swapBridgeViaStargateNative.ts | 35 +++- scripts/e2e/utils/erc20.ts | 1 + scripts/e2e/utils/reproducibility.ts | 106 +++++++++++ 10 files changed, 531 insertions(+), 10 deletions(-) create mode 100644 scripts/e2e/approveViaModular.ts create mode 100644 scripts/e2e/swapBridgeViaCctpSimple.ts create mode 100644 scripts/e2e/utils/reproducibility.ts diff --git a/scripts/e2e/approveViaModular.ts b/scripts/e2e/approveViaModular.ts new file mode 100644 index 0000000..3d9f34e --- /dev/null +++ b/scripts/e2e/approveViaModular.ts @@ -0,0 +1,134 @@ +/** + * Script — Call ERC-20 approve(spender, amount) through the router using + * `performModularExecution(Action[])`. + * + * This routes a single CALL action targeting the token contract so the router + * itself issues the approval — useful when the router holds tokens and needs + * to authorise a downstream spender (e.g. a bridge contract) before calling it. + * + * Usage: + * TOKEN=0x... SPENDER=0x... AMOUNT=1000000 PRIVATE_KEY=0x... \ + * ts-node scripts/e2e/approveViaModular.ts + * + * Optional: + * CHAIN_ID=137 (default: 137, Polygon) + * AMOUNT=max (uses MaxUint256) + * + * actionInfo packing (from the contract): + * bits 0-7 : callType (0 = CALL) + * bits 8-15 : storeResult flag (0 = don't store) + * bits 16+ : target address (uint160) + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { CHAIN_IDS, routerAddressForChain, RPC, TOKENS } from './config'; +import { encodeApprove } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; + +// ─── actionInfo helpers ─────────────────────────────────────────────────────── + +const CallType = { CALL: 0n, STATICCALL: 1n, CALL_WITH_NATIVE: 2n } as const; + +function packActionInfo( + target: string, + callType = CallType.CALL, + storeResult = false, +): bigint { + return (BigInt(target) << 16n) | (storeResult ? 0x100n : 0n) | callType; +} + +// ─── build + send ───────────────────────────────────────────────────────────── + +async function run(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const token = TOKENS.USDC_POLYGON_CIRCLE; + if (!token || !/^0x[a-fA-F0-9]{40}$/.test(token)) { + throw new Error( + 'TOKEN env var required (checksummed or lowercase ERC-20 address)', + ); + } + + const spender = '0x28b5a0e9c621a5badaa536219b3a228c8168cf5d'; // cctp tokenmessengerv2 + if (!spender || !/^0x[a-fA-F0-9]{40}$/.test(spender)) { + throw new Error( + 'SPENDER env var required (checksummed or lowercase address)', + ); + } + + const amount = ethers.MaxUint256; + + const chainId = CHAIN_IDS.POLYGON; + const rpcUrl: string = + process.env.RPC_URL ?? + (chainId === CHAIN_IDS.POLYGON + ? RPC.POLYGON + : chainId === CHAIN_IDS.ARBITRUM + ? RPC.ARBITRUM + : chainId === CHAIN_IDS.BASE + ? RPC.BASE + : chainId === CHAIN_IDS.ETHEREUM + ? RPC.ETHEREUM + : (() => { + throw new Error(`No default RPC for chain ${chainId}; set RPC_URL`); + })()); + + const routerAddress = routerAddressForChain(chainId); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + const approveCalldata = encodeApprove(spender, amount); + + // Single action: CALL token.approve(spender, amount) from the router. + const actions = [ + { + actionInfo: packActionInfo(token), + data: approveCalldata, + splices: [], + }, + ]; + + const calldata = routerIface.encodeFunctionData('performModularExecution', [ + actions, + ]); + + console.log(`Signer: ${signerAddress}`); + console.log(`Chain: ${chainId}`); + console.log(`Router: ${routerAddress}`); + console.log(`Token: ${token}`); + console.log(`Spender: ${spender}`); + console.log( + `Amount: ${ + amount === ethers.MaxUint256 ? 'MaxUint256' : amount.toString() + }`, + ); + console.log('Sending performModularExecution → token.approve...'); + + const tx = await signer.sendTransaction({ + to: routerAddress, + data: calldata, + }); + console.log(`Tx hash: ${tx.hash}`); + const receipt = await tx.wait(); + console.log( + `Status: ${receipt?.status === 1 ? 'success' : 'reverted'} (block ${ + receipt?.blockNumber + })`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); + +async function main(): Promise { + await run(); +} diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index 1eef43e..2a73641 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -44,6 +44,10 @@ import { MonolithicExecution, NO_FEE, NO_SWAP } from './utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from './utils/reproducibility'; /** Router on Polygon — quotes + modular recipient target must match executing chain deployment. */ const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -142,6 +146,9 @@ async function executeLeg(args: { console.log(`Relay spender: ${relaySpender}`); console.log(`Deposit target: ${depositTarget}`); + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, relaySpender); + let execCalldata: string; if (useModular) { const actions = buildModularActions( @@ -285,6 +292,9 @@ async function executeLegUsdcPolygonToBase(args: { console.log(`Relay spender: ${relaySpender}`); console.log(`Deposit target: ${depositTarget}`); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, relaySpender); + let execCalldata: string; if (useModular) { const actions = buildModularActionsUsdcPolygonToBase( @@ -360,7 +370,7 @@ async function mainUsdcPolygonToBaseRelay() { ); } - const legAmount = walletBalance / 2n; + const legAmount = (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error( `Wallet balance (${walletBalance}) is too small to split into two nonzero halves.`, @@ -431,7 +441,7 @@ async function main() { ); } - const legAmount = walletBalance / 2n; + const legAmount = (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error( `Wallet balance (${walletBalance}) is too small to split into two nonzero halves.`, diff --git a/scripts/e2e/bridgeViaRelaySimple.ts b/scripts/e2e/bridgeViaRelaySimple.ts index d88ce24..1077fbf 100644 --- a/scripts/e2e/bridgeViaRelaySimple.ts +++ b/scripts/e2e/bridgeViaRelaySimple.ts @@ -39,6 +39,10 @@ import { ROUTER_ABI } from './utils/routerAbi'; import type { InputData, StaticBridgeData } from './utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from './utils/reproducibility'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -140,6 +144,9 @@ async function executeLeg(args: { console.log(`Relay spender: ${relaySpender}`); console.log(`Deposit target: ${depositTarget}`); + await ensureRouterErc20Balance(signer, config.inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, config.inputToken, relaySpender); + const execCalldata = buildBridgeCalldata(routerIface, { signerAddress, inputToken: config.inputToken, @@ -221,7 +228,7 @@ async function run(useUsdc: boolean): Promise { config: legConfig, signer, signerAddress, - inputAmount: walletBalance, + inputAmount: (walletBalance - 20n) / 2n, routerIface, }); diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index 767d0c0..30ac4a1 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -65,6 +65,11 @@ import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, NO_FEE, ZERO_ADDRESS } from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from './utils/reproducibility'; // ─── Exec-mode selection ────────────────────────────────────────────────────── @@ -302,6 +307,10 @@ async function executeLeg( console.log(` Fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); console.log(` Min ETH out: ${ethers.formatEther(minAmountOut)} ETH`); + await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, routerAddress); + await ensureRouterNativeBalance(signer, routerAddress); + await ensureRouterApproval(signer, routerAddress, TOKENS.AAVE_ETH, ooRouter); + const arbFee = await estimateArbitrumBridgeFee(provider); const minEthRequired = feeAmount + arbFee; if (estimatedOut < minEthRequired) { @@ -388,7 +397,7 @@ async function main(): Promise { ); } - const legAmount = fullBalance / 2n; + const legAmount = (fullBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error('AAVE balance too small to split into two legs.'); } diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index 3ff723e..e1afc98 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -41,6 +41,10 @@ import { } from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from './utils/reproducibility'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -271,6 +275,9 @@ async function executeLegUsdcPolygonToBaseCctp(args: { true, ); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + let execCalldata: string; if (useModular) { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ @@ -353,6 +360,11 @@ async function executeLeg(args: { true, ); + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouterAddress); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + let execCalldata: string; if (useModular) { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ @@ -422,7 +434,7 @@ async function mainUsdcPolygonToBaseCctp() { ); } - const legAmount = walletBalance / 2n; + const legAmount = (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error( `Balance ${walletBalance} too small for two nonzero 50% legs.`, @@ -490,7 +502,7 @@ async function main() { ); } - const legAmount = walletBalance / 2n; + const legAmount = (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error( `Balance ${walletBalance} too small for two nonzero 50% legs.`, diff --git a/scripts/e2e/swapBridgeViaCctpSimple.ts b/scripts/e2e/swapBridgeViaCctpSimple.ts new file mode 100644 index 0000000..46ae4f6 --- /dev/null +++ b/scripts/e2e/swapBridgeViaCctpSimple.ts @@ -0,0 +1,199 @@ +/** + * Polygon native USDC → Base USDC via CCTP v2 using `router.bridge(...)`. + * + * Same burn token / TokenMessenger constraints as {@link swapBridgeViaCctp}: + * use Circle’s native Polygon USDC (`USDC_POLYGON_CIRCLE`); bridged USDC.e is unsupported. + * + * Unlike the monolithic/modular paths in `swapBridgeViaCctp.ts`, this script: + * – only supports USDC-in (no OpenOcean AAVE→USDC swap); + * – encodes the net `depositForBurn` amount in calldata up front (no splice); + * – uses a single `bridge` entrypoint per run (full wallet balance by default). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctpSimple.ts + * + * No pre-bridge fee: + * FEE_AMOUNT_BPS=0 PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctpSimple.ts + * + * Router: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(137)` in config.ts. + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, +} from './config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; +import { getWalletErc20Balance } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import type { InputData, StaticBridgeData } from './utils/contractTypes'; +import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from './utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function encodeFeeBytes(receiver: string, feeAmount: bigint): string { + if (feeAmount === 0n) { + return '0x'; + } + return ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256'], + [receiver, feeAmount], + ); +} + +/** + * CCTP `depositForBurn` with explicit burn amount (net after optional fee). + */ +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, + amount: bigint, + fastPath: boolean = true, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + + const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); + const maxFee = fastPath ? 1_000_000n : 0n; + const minFinalityThreshold = fastPath ? 1000 : 2000; + + return iface.encodeFunctionData('depositForBurn', [ + amount, + destinationCctpDomain, + mintRecipient, + burnToken, + ethers.ZeroHash, + maxFee, + minFinalityThreshold, + ]); +} + +function buildBridgeCalldata( + routerIface: ethers.Interface, + args: { + signerAddress: string; + inputToken: string; + inputAmount: bigint; + feeBytes: string; + tokenMessenger: string; + depositData: string; + }, +): string { + const input: InputData = { + user: args.signerAddress, + inputToken: args.inputToken, + inputAmount: args.inputAmount, + }; + + const bridgeData: StaticBridgeData = { + target: args.tokenMessenger, + approvalSpender: args.tokenMessenger, + value: 0n, + data: args.depositData, + }; + + return routerIface.encodeFunctionData('bridge', [ + input, + args.feeBytes, + bridgeData, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance( + inputToken, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error( + `Signer ${signerAddress} has zero Circle native USDC on Polygon. Fund ${inputToken} on Polygon PoS.`, + ); + } + + const inputAmount = walletBalance - 20n; + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Input USDC: ${ethers.formatUnits(inputAmount, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + console.log(`TokenMessenger: ${polyCctp.tokenMessenger}`); + console.log(`Burn token: ${polyCctp.usdcAddress}`); + + const depositData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + bridgeAmount, + true, + ); + + const feeBytes = encodeFeeBytes(signerAddress, feeAmount); + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = buildBridgeCalldata(routerIface, { + signerAddress, + inputToken, + inputAmount, + feeBytes, + tokenMessenger: polyCctp.tokenMessenger, + depositData, + }); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, polyCctp.tokenMessenger); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + ); + + logTxnSummary( + 'Polygon USDC → Base USDC — CCTP — Simple bridge', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swapBridgeViaOft.ts b/scripts/e2e/swapBridgeViaOft.ts index f9fe823..0bc8725 100644 --- a/scripts/e2e/swapBridgeViaOft.ts +++ b/scripts/e2e/swapBridgeViaOft.ts @@ -71,6 +71,10 @@ import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, NO_FEE, NO_SWAP } from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from './utils/reproducibility'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -372,6 +376,11 @@ async function executeCase1Leg(args: { ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`, ); + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); let execCalldata: string; @@ -558,6 +567,9 @@ async function executeCase2Leg(args: { ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`, ); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); let execCalldata: string; @@ -640,7 +652,7 @@ async function runCase1( ); } - const legAmount = walletBalance / 2n; + const legAmount = (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error('Case 1: AAVE balance too small to split into two halves.'); } @@ -702,7 +714,7 @@ async function runCase2( ); } - const legAmount = walletBalance / 2n; + const legAmount = (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error( 'Case 2: USDT0 balance too small to split into two halves.', diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index ad9cc13..19384bf 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -91,6 +91,11 @@ import type { ModularAction } from './utils/modularActionsBuilder/index'; import { MonolithicExecution, NO_FEE, NO_SWAP, ZERO_ADDRESS } from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from './utils/reproducibility'; /** * LZ extra options for Polygon USDT0 OFT Adapter `send()` (executor gas). @@ -900,6 +905,32 @@ async function executeLeg( inputAmountWei = maxAffordableSwapIn; } + // ── State prep for reproducible gas ───────────────────────────────────────── + // Determine the token the router will approve to the bridge contract (null for native pools). + const bridgeToken: string | null = + cfg.isNativePool + ? null + : cfg.ooSwap !== null + ? cfg.ooSwap.outToken + : cfg.inputToken; + + if (!cfg.isNativeInput) { + await ensureRouterErc20Balance(signer, cfg.inputToken, routerAddress); + } + if (cfg.isNativeInput || cfg.isNativePool || (cfg.ooSwap && cfg.ooSwap.outToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase())) { + await ensureRouterNativeBalance(signer, routerAddress); + } + if (cfg.ooSwap && cfg.ooSwap.outToken.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) { + await ensureRouterErc20Balance(signer, cfg.ooSwap.outToken, routerAddress); + } + if (cfg.ooSwap && !cfg.isNativeInput) { + await ensureRouterApproval(signer, routerAddress, cfg.inputToken, ooRouter); + } + if (bridgeToken && bridgeToken.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) { + await ensureRouterApproval(signer, routerAddress, bridgeToken, cfg.bridgeContract); + } + // ──────────────────────────────────────────────────────────────────────────── + let amountLD: bigint; if (cfg.isNativePool) { amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; @@ -997,7 +1028,7 @@ async function runCase( ); } // Reserve wei for signer gas; lz fee itself is deducted inside executeLeg (`txValue = swap + fee`). - walletBalance = raw - NATIVE_INPUT_GAS_RESERVE; + walletBalance = raw - NATIVE_INPUT_GAS_RESERVE - 20n; decimals = 18; } else { ({ balance: walletBalance, decimals } = await getWalletErc20Balance( @@ -1012,7 +1043,7 @@ async function runCase( ); } - const legAmount = walletBalance / 2n; + const legAmount = cfg.isNativeInput ? walletBalance / 2n : (walletBalance - 20n) / 2n; if (legAmount === 0n) { throw new Error(`${cfg.name}: balance too small to split into two halves.`); } diff --git a/scripts/e2e/utils/erc20.ts b/scripts/e2e/utils/erc20.ts index 7716911..87484af 100644 --- a/scripts/e2e/utils/erc20.ts +++ b/scripts/e2e/utils/erc20.ts @@ -44,6 +44,7 @@ export function getErc20Contract(tokenAddress: string, providerOrSigner: ethers. tokenAddress, [ 'function approve(address spender, uint256 amount) external returns (bool)', + 'function transfer(address recipient, uint256 amount) external returns (bool)', 'function allowance(address owner, address spender) external view returns (uint256)', 'function balanceOf(address account) external view returns (uint256)', 'function decimals() external view returns (uint8)', diff --git a/scripts/e2e/utils/reproducibility.ts b/scripts/e2e/utils/reproducibility.ts new file mode 100644 index 0000000..6252190 --- /dev/null +++ b/scripts/e2e/utils/reproducibility.ts @@ -0,0 +1,106 @@ +/** + * State-prep helpers for reproducible on-chain gas-cost tests. + * + * Callers must pass the deployed open-router address from config (`routerAddressForChain`, etc.), + * never Relay `depositTarget`, CCTP `tokenMessenger`, or other external calldata targets. + * + * Before each test leg these ensure: + * 1. The router holds ≥ 20 wei of every token whose balance slot will be written. + * 2. The router has a non-zero ERC-20 allowance for every external spender it will call. + * + * Seeding slots to non-zero means subsequent SSTORE writes cost ~2 900 gas + * (non-zero → non-zero) rather than ~20 000 gas (zero → non-zero), giving + * consistent gas readings across repeated runs. + */ +import { ethers } from 'ethers'; +import { getErc20Contract, encodeApprove } from './erc20'; +import { ROUTER_ABI } from './routerAbi'; + +const SEED_WEI = 20n; + +function packCallAction(target: string): bigint { + return BigInt(target) << 16n; // CallType.CALL=0, storeResult=false +} + +/** + * Transfers {@link SEED_WEI} of `token` from `signer` to the deployed open router only + * when that router already holds zero — never to Relay/deposit/spender contracts. + */ +export async function ensureRouterErc20Balance( + signer: ethers.Wallet, + token: string, + openRouterAddress: string, +): Promise { + const openRouter = ethers.getAddress(openRouterAddress); + const tokenResolved = ethers.getAddress(token); + const tokenRo = getErc20Contract(tokenResolved, signer.provider!); + const bal = BigInt(await tokenRo.balanceOf(openRouter)); + if (bal > 0n) { + return; + } + + console.log( + ` [state-prep] open router ${openRouter} token ${tokenResolved} balance=0 — signer transfer ${SEED_WEI} wei to open router only`, + ); + const tx = await getErc20Contract(tokenResolved, signer).transfer(openRouter, SEED_WEI); + await tx.wait(); +} + +/** + * Sends {@link SEED_WEI} of native currency from `signer` to the open router when its + * balance is zero; skipped when already non-zero. + */ +export async function ensureRouterNativeBalance( + signer: ethers.Wallet, + openRouterAddress: string, +): Promise { + const openRouter = ethers.getAddress(openRouterAddress); + const bal = await signer.provider!.getBalance(openRouter); + if (bal > 0n) { + return; + } + + console.log( + ` [state-prep] open router ${openRouter} native balance=0 — signer sending ${SEED_WEI} wei to open router only`, + ); + const tx = await signer.sendTransaction({ to: openRouter, value: SEED_WEI }); + await tx.wait(); +} + +/** + * Issues `token.approve(spender, MaxUint256)` FROM the router (via + * `performModularExecution`) when the current router→spender allowance is zero. + * + * Guarantees the allowance slot is non-zero before the test txn so that the + * approval write inside the test costs ~2 900 gas (non-zero → non-zero). + */ +export async function ensureRouterApproval( + signer: ethers.Wallet, + openRouterAddress: string, + token: string, + spender: string, +): Promise { + const openRouter = ethers.getAddress(openRouterAddress); + const tokenResolved = ethers.getAddress(token); + const spenderResolved = ethers.getAddress(spender); + const tokenRo = getErc20Contract(tokenResolved, signer.provider!); + const allowance = BigInt(await tokenRo.allowance(openRouter, spenderResolved)); + if (allowance > 0n) { + return; + } + + console.log( + ` [state-prep] open router ${openRouter} token ${tokenResolved} allowance for ${spenderResolved}=0 — pre-approving MaxUint256 via open router`, + ); + const routerIface = new ethers.Interface(ROUTER_ABI); + const actions = [ + { + actionInfo: packCallAction(tokenResolved), + data: encodeApprove(spenderResolved, ethers.MaxUint256), + splices: [], + }, + ]; + const calldata = routerIface.encodeFunctionData('performModularExecution', [actions]); + const tx = await signer.sendTransaction({ to: openRouter, data: calldata }); + await tx.wait(); +} From 17dc785d7c350f1bd7db02ae08ce3a913d680292 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 20:46:26 +0530 Subject: [PATCH 31/69] feat: introduce flags for fee and balance handling in swap functions - Added a `flags` uint8 to manage fee timing and output measurement in `swap` and `swapAndBridge` functions. - Implemented `FEE_FLAG_BIT_MASK` and `BALANCE_FLAG_BIT_MASK` for enhanced control over fee collection and output calculation. - Updated documentation to clarify the usage of flags and their impact on swap behavior. --- src/combined/BungeeOpenRouterV2Unchecked.sol | 235 +++++++++++++++++-- 1 file changed, 212 insertions(+), 23 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index a836eb2..d4bcaf5 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -85,6 +85,9 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { SwapData swap; FeeData postFee; BridgeData bridge; + /// Packed byte; monolithic pipeline only tests `BALANCE_FLAG_BIT_MASK` (bit 1) in `_execSwap`. + /// Fee timing uses `preFee` / `postFee` structs — `FEE_FLAG_BIT_MASK` (bit 0) is ignored here. + uint8 flags; } // ========================================================================= @@ -103,6 +106,43 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { uint256[] splices; } + // ========================================================================= + // Flags (swap / swapAndBridge / monolithic swap step) + // ========================================================================= + // + // Instead of two bool parameters, one uint8 packs independent switches; future flags can use + // bit 2 (0x04), bit 3 (0x08), etc. without changing the ABI shape. + // + // Bit layout (least significant bits); test with `(flags & MASK) != 0`: + // bits 7..2 : reserved (0) + // bit 1 : BALANCE_FLAG_BIT_MASK (0x02) — swap output: returndata vs balance delta + // bit 0 : FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap (standalone paths only) + // + // Combined values for swap()/swapAndBridge(): + // + // flags binary (low byte) postFee? balance-of output? + // ───── ────────────────── ──────── ────────────────── + // 0x00 00000000 no returndata word + // 0x01 00000001 yes returndata word + // 0x02 00000010 no balance delta on outputToken + // 0x03 00000011 yes balance delta on outputToken + // + // FEE_FLAG_BIT_MASK selects bit 0 — fee timing (see `_collectFee` + swap flow). + // Cleared — pull → deduct fee from input token → swap remainder → standalone swap skips minOutput. + // Set — pull → swap full input → deduct fee from output token → standalone swap checks minOutput. + // + // BALANCE_FLAG_BIT_MASK selects bit 1 — swap output sizing. + // Cleared — decode returned amount from call returndata at `swapData.returnDataWordOffset`. + // Set — snapshot outputToken balance before call, measure (after − before) as output. + // + // Monolithic `performExecution` applies only `BALANCE_FLAG_BIT_MASK` in `_execSwap`; fee timing is `preFee`/`postFee` structs. + + /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. + uint8 internal constant FEE_FLAG_BIT_MASK = 0x01; + + /// @dev Bit mask 0x02: measure swap output by balance delta when `(flags & mask) != 0`; clear = returndata word. + uint8 internal constant BALANCE_FLAG_BIT_MASK = 0x02; + // ========================================================================= // Errors // ========================================================================= @@ -137,11 +177,128 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * @dev The caller MUST route through `AllowanceHolder.exec` so that * `_msgSender()` resolves to `exec.input.user`. There is no nonce or * deadline; replay protection is the caller's responsibility. + * Bit 0 (`FEE_FLAG_BIT_MASK`) is unused in monolithic runs; fee placement is `preFee` / `postFee` structs. + * `exec.flags` only contributes `BALANCE_FLAG_BIT_MASK` to the optional `_execSwap` step. */ function performExecution(MonolithicExecution calldata exec) external payable { _runMonolithic(exec); } + // ========================================================================= + // External: standalone swap + // ========================================================================= + + /** + * @notice Pull → optional pre/post fee → swap. + * @param flags Packed `uint8`; OR masks then test with `flags & MASK != 0`. Masks: `FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` (0x02). + * Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), + * `2` (pre-fee, balance delta), `3` (post-fee, balance delta). + * @param feeBytes 0x = no fee; 64 bytes = abi.encode(address receiver, uint256 amount). + * @dev minOutput is only enforced in post-fee mode. Pre-fee skips minOutput check. + * Post-fee: fee collected from output token after swap, then minOutput validated. + * Pre-fee: fee collected from input token before swap, minOutput skipped. + * Bits are read with bitwise AND against each mask; omitting both flags ⇒ pre-fee + returndata. + */ + function swap( + InputData calldata input, + uint8 flags, + bytes calldata feeBytes, + SwapData calldata swapData + ) external payable returns (uint256 finalAmount) { + if (input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0)) { + revert InvalidExecution(); + } + + _pullFromUser(input.inputToken, input.user, input.inputAmount); + + // Check feeBytes first: flag bit is only read when a fee is actually present. + // FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output + bool hasFee = feeBytes.length != 0; + /// @dev if hasFee is false, we short-circuit and flag check wont execute at runtime saving gas + bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; + uint256 swapInput = input.inputAmount; + + if (hasFee && !postFee) { + uint256 fee = _collectFee(input.inputToken, feeBytes); + if (fee > swapInput) revert InsufficientFunds(); + unchecked { swapInput -= fee; } + } + + if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + } + + // BALANCE_FLAG_BIT_MASK: unset ⇒ decode output word from returndata; set ⇒ output = delta on outputToken + finalAmount = _execSwap(swapData, flags & BALANCE_FLAG_BIT_MASK != 0); + + if (postFee) { + uint256 fee = _collectFee(swapData.outputToken, feeBytes); + if (fee > finalAmount) revert InsufficientFunds(); + unchecked { finalAmount -= fee; } + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + } + } + + // ========================================================================= + // External: swap + bridge + // ========================================================================= + + /** + * @notice Pull → optional pre/post swap fee → swap → bridge with runtime amount splicing. + * @param flags Same packing as `swap`: 0x00–0x03 as documented on the flag constants block. + * @param feeBytes 0x = no fee; 64 bytes = abi.encode(address receiver, uint256 amount). + * @dev minOutput is always enforced (after fee deduction in post-fee mode). + * Post-fee: fee collected from output token after swap, then minOutput validated. + * Pre-fee: fee collected from input token before swap, minOutput still validated. + */ + function swapAndBridge( + InputData calldata input, + uint8 flags, + bytes calldata feeBytes, + SwapData calldata swapData, + BridgeData calldata bridgeData + ) external payable { + if ( + bridgeData.target == address(0) || input.user == address(0) || + input.inputToken == address(0) || swapData.target == address(0) + ) { + revert InvalidExecution(); + } + + _pullFromUser(input.inputToken, input.user, input.inputAmount); + + // Check feeBytes first: flag bit is only read when a fee is actually present. + // FEE_FLAG_BIT_MASK: same semantics as standalone `swap` (fee from input vs output token) + bool hasFee = feeBytes.length != 0; + bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; + uint256 swapInput = input.inputAmount; + + if (hasFee && !postFee) { + uint256 fee = _collectFee(input.inputToken, feeBytes); + if (fee > swapInput) revert InsufficientFunds(); + unchecked { swapInput -= fee; } + } + + if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + } + + address finalToken = swapData.outputToken; + // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` + uint256 finalAmount = _execSwap(swapData, flags & BALANCE_FLAG_BIT_MASK != 0); + + if (postFee) { + uint256 fee = _collectFee(finalToken, feeBytes); + if (fee > finalAmount) revert InsufficientFunds(); + unchecked { finalAmount -= fee; } + } + + // Always check minOutput (unlike standalone swap where pre-fee skips this) + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + + _doBridge(finalToken, finalAmount, bridgeData); + } + // ========================================================================= // External: simple bridge path (no swap) // ========================================================================= @@ -263,43 +420,75 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } } - // 5. splice finalAmount into bridge calldata at every signed offset - bytes memory bridgeData = exec.bridge.data; - BytesSpliceLib.spliceWords({data: bridgeData, positions: exec.bridge.amountPositions, word: finalAmount}); - - // 6. optional approval to bridge spender - if (exec.bridge.approvalSpender != address(0) && finalToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(finalToken, exec.bridge.approvalSpender, finalAmount); - } - - // 7. bridge call, bubbling any revert - // when useFinalAmountAsValue is set, forward finalAmount as msg.value so - // native-token bridges (e.g. Arbitrum inbox) receive the exact bridged amount. - uint256 bridgeValue = exec.bridge.useFinalAmountAsValue ? finalAmount : exec.bridge.value; - _doCall(exec.bridge.target, bridgeValue, bridgeData, false); + // 5. bridge: splice, approve, call + _doBridge(finalToken, finalAmount, exec.bridge); } - /// @dev Swap helper; decodes final amount from a returndata word. function _performSwap(MonolithicExecution calldata exec) internal returns (address finalToken, uint256 finalAmount) { if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 swapInput; - unchecked { - swapInput = exec.input.inputAmount - exec.preFee.amount; - } + unchecked { swapInput = exec.input.inputAmount - exec.preFee.amount; } SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); } - bytes memory ret = _doCall(exec.swap.target, exec.swap.value, exec.swap.data, true); - finalAmount = _decodeReturnWord(ret, exec.swap.returnDataWordOffset); + // Monolithic path: only `BALANCE_FLAG_BIT_MASK` is read for `_execSwap`; fee uses `preFee` / `postFee`, not bit 0. + finalAmount = _execSwap(exec.swap, exec.flags & BALANCE_FLAG_BIT_MASK != 0); + if (finalAmount < exec.swap.minOutput) revert SwapOutputInsufficient(); + finalToken = exec.swap.outputToken; + } + + // ========================================================================= + // Internal: swap / fee / bridge helpers + // ========================================================================= - if (finalAmount < exec.swap.minOutput) { - revert SwapOutputInsufficient(); + /// @dev Execute swap; output measured via returndata word or output-token balance delta. + /// useBalanceOf=true: measure output as (balance after - balance before). + /// useBalanceOf=false: decode output from returndata at swapData.returnDataWordOffset. + function _execSwap(SwapData calldata swapData, bool useBalanceOf) internal returns (uint256 finalAmount) { + if (useBalanceOf) { + // Balance delta mode: snapshot before, call, measure delta + uint256 before = CurrencyLib.balanceOf(swapData.outputToken, address(this)); + _doCall(swapData.target, swapData.value, swapData.data, false); + finalAmount = CurrencyLib.balanceOf(swapData.outputToken, address(this)) - before; + } else { + // Returndata mode: decode output from a specific word in returndata + bytes memory ret = _doCall(swapData.target, swapData.value, swapData.data, true); + finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); } + } - finalToken = exec.swap.outputToken; + /// @dev Decode, validate, and collect fee from feeBytes. Returns fee amount (0 if feeBytes empty). + /// feeBytes encoding: + /// - 0x (zero length): no fee, return 0 immediately. + /// - 64 bytes: abi.encode(address receiver, uint256 amount). Transfer amount to receiver. + /// Caller must pass the correct `token` address: + /// - Pre-fee: pass inputToken (fee deducted before swap). + /// - Post-fee: pass outputToken (fee deducted after swap). + function _collectFee(address token, bytes calldata feeBytes) internal returns (uint256 feeAmount) { + if (feeBytes.length != 64) revert InvalidExecution(); + address receiver; + assembly ("memory-safe") { + receiver := calldataload(feeBytes.offset) + feeAmount := calldataload(add(feeBytes.offset, 0x20)) + } + if (feeAmount != 0) CurrencyLib.transfer(token, receiver, feeAmount); + } + + /// @dev Splice finalAmount into bridge calldata, approve, and call bridge target. + function _doBridge(address token, uint256 amount, BridgeData calldata bd) internal { + bytes memory bData = bd.data; + BytesSpliceLib.spliceWords({data: bData, positions: bd.amountPositions, word: amount}); + + if (bd.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(token, bd.approvalSpender, amount); + } + + // when useFinalAmountAsValue, forward amount as msg.value for native-token bridges + uint256 bridgeValue = bd.useFinalAmountAsValue ? amount : bd.value; + _doCall(bd.target, bridgeValue, bData, false); } // ========================================================================= From 06853084fee1a7a21e1ac178c57432a99ad71f3e Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 21:26:33 +0530 Subject: [PATCH 32/69] fix: swap output check after swap --- src/combined/BungeeOpenRouterV2Unchecked.sol | 70 ++++++++++++-------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index d4bcaf5..2e4327f 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -128,8 +128,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // 0x03 00000011 yes balance delta on outputToken // // FEE_FLAG_BIT_MASK selects bit 0 — fee timing (see `_collectFee` + swap flow). - // Cleared — pull → deduct fee from input token → swap remainder → standalone swap skips minOutput. - // Set — pull → swap full input → deduct fee from output token → standalone swap checks minOutput. + // Cleared — pull → deduct fee from input token → swap remainder. + // Set — pull → swap full input → deduct fee from output token (after minOutput check on swap result). // // BALANCE_FLAG_BIT_MASK selects bit 1 — swap output sizing. // Cleared — decode returned amount from call returndata at `swapData.returnDataWordOffset`. @@ -138,7 +138,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Monolithic `performExecution` applies only `BALANCE_FLAG_BIT_MASK` in `_execSwap`; fee timing is `preFee`/`postFee` structs. /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. - uint8 internal constant FEE_FLAG_BIT_MASK = 0x01; + uint8 internal constant FEE_FLAG_BIT_MASK = 0x01; /// @dev Bit mask 0x02: measure swap output by balance delta when `(flags & mask) != 0`; clear = returndata word. uint8 internal constant BALANCE_FLAG_BIT_MASK = 0x02; @@ -194,17 +194,16 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), * `2` (pre-fee, balance delta), `3` (post-fee, balance delta). * @param feeBytes 0x = no fee; 64 bytes = abi.encode(address receiver, uint256 amount). - * @dev minOutput is only enforced in post-fee mode. Pre-fee skips minOutput check. - * Post-fee: fee collected from output token after swap, then minOutput validated. - * Pre-fee: fee collected from input token before swap, minOutput skipped. - * Bits are read with bitwise AND against each mask; omitting both flags ⇒ pre-fee + returndata. + * @dev `minOutput` is the minimum gross amount coming out of the swap (before any output-token fee). + * It is enforced immediately after `_execSwap`, then post-swap fee (if any) is collected. + * Pre-fee paths take the input-side fee before the swap; `minOutput` still guards the swap outcome. + * Bits are read with bitwise AND against each mask; omitting both masks ⇒ pre-fee + returndata. */ - function swap( - InputData calldata input, - uint8 flags, - bytes calldata feeBytes, - SwapData calldata swapData - ) external payable returns (uint256 finalAmount) { + function swap(InputData calldata input, uint8 flags, bytes calldata feeBytes, SwapData calldata swapData) + external + payable + returns (uint256 finalAmount) + { if (input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0)) { revert InvalidExecution(); } @@ -218,24 +217,33 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; uint256 swapInput = input.inputAmount; + // collect pre-swap fee if (hasFee && !postFee) { uint256 fee = _collectFee(input.inputToken, feeBytes); if (fee > swapInput) revert InsufficientFunds(); - unchecked { swapInput -= fee; } + unchecked { + swapInput -= fee; + } } + // approve swap router if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); } // BALANCE_FLAG_BIT_MASK: unset ⇒ decode output word from returndata; set ⇒ output = delta on outputToken + // perform swap finalAmount = _execSwap(swapData, flags & BALANCE_FLAG_BIT_MASK != 0); + // check swap output + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + // collect post-swap fee if (postFee) { uint256 fee = _collectFee(swapData.outputToken, feeBytes); if (fee > finalAmount) revert InsufficientFunds(); - unchecked { finalAmount -= fee; } - if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + unchecked { + finalAmount -= fee; + } } } @@ -247,9 +255,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * @notice Pull → optional pre/post swap fee → swap → bridge with runtime amount splicing. * @param flags Same packing as `swap`: 0x00–0x03 as documented on the flag constants block. * @param feeBytes 0x = no fee; 64 bytes = abi.encode(address receiver, uint256 amount). - * @dev minOutput is always enforced (after fee deduction in post-fee mode). - * Post-fee: fee collected from output token after swap, then minOutput validated. - * Pre-fee: fee collected from input token before swap, minOutput still validated. + * @dev Same `minOutput` rule as `swap`: validated on gross `_execSwap` output, then optional output fee applies. */ function swapAndBridge( InputData calldata input, @@ -259,8 +265,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { BridgeData calldata bridgeData ) external payable { if ( - bridgeData.target == address(0) || input.user == address(0) || - input.inputToken == address(0) || swapData.target == address(0) + bridgeData.target == address(0) || input.user == address(0) || input.inputToken == address(0) + || swapData.target == address(0) ) { revert InvalidExecution(); } @@ -273,29 +279,37 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; uint256 swapInput = input.inputAmount; + // collect pre-swap fee if (hasFee && !postFee) { uint256 fee = _collectFee(input.inputToken, feeBytes); if (fee > swapInput) revert InsufficientFunds(); - unchecked { swapInput -= fee; } + unchecked { + swapInput -= fee; + } } + // approve swap router if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); } address finalToken = swapData.outputToken; + // perform swap // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` uint256 finalAmount = _execSwap(swapData, flags & BALANCE_FLAG_BIT_MASK != 0); + // check swap output + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + + // collect post-swap fee if (postFee) { uint256 fee = _collectFee(finalToken, feeBytes); if (fee > finalAmount) revert InsufficientFunds(); - unchecked { finalAmount -= fee; } + unchecked { + finalAmount -= fee; + } } - // Always check minOutput (unlike standalone swap where pre-fee skips this) - if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); - _doBridge(finalToken, finalAmount, bridgeData); } @@ -430,7 +444,9 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { { if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 swapInput; - unchecked { swapInput = exec.input.inputAmount - exec.preFee.amount; } + unchecked { + swapInput = exec.input.inputAmount - exec.preFee.amount; + } SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); } From c45a025e60df18c0d7b9f7000e5d4dee5d4b9c4a Mon Sep 17 00:00:00 2001 From: arthcp Date: Fri, 15 May 2026 20:22:58 +0400 Subject: [PATCH 33/69] feat: calldata optimisations --- scripts/e2e/bridgeViaRelay.ts | 90 ++--- scripts/e2e/bridgeViaRelaySimple.ts | 42 +-- scripts/e2e/swapBridgeViaArbitrumNative.ts | 55 +-- scripts/e2e/swapBridgeViaCctp.ts | 107 +++--- scripts/e2e/swapBridgeViaCctpSimple.ts | 24 +- scripts/e2e/swapBridgeViaOft.ts | 115 +++--- scripts/e2e/swapBridgeViaStargateNative.ts | 136 +++---- scripts/e2e/utils/contractTypes.ts | 41 ++- scripts/e2e/utils/routerAbi.ts | 14 +- src/combined/BungeeOpenRouterV2Unchecked.sol | 354 +++++++++++-------- test/poc/OneInchCctpOpenRouterPoC.t.sol | 21 +- 11 files changed, 540 insertions(+), 459 deletions(-) diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index 2a73641..e464516 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -40,7 +40,7 @@ import { import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { MonolithicExecution, NO_FEE, NO_SWAP } from './utils/contractTypes'; +import { MonolithicExecutionCall, NO_FEE, NO_SWAP, monolithicArgs } from './utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; @@ -59,27 +59,29 @@ function buildMonolithicExecution( relaySpender: string, depositTarget: string, depositData: string, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { - user: signerAddress, - inputToken: TOKENS.AAVE_POLYGON, - inputAmount, - }, - preFee: { - receiver: signerAddress, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: depositTarget, - approvalSpender: relaySpender, - value: 0n, - data: depositData, - amountPositions: [], - useFinalAmountAsValue: false, + exec: { + input: { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount, + }, + preFee: { + receiver: signerAddress, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: depositTarget, + approvalSpender: relaySpender, + value: 0n, + }, + flags: 0n, }, + swapCallData: '0x', + bridgeCallData: depositData, }; } @@ -173,9 +175,7 @@ async function executeLeg(args: { depositTarget, depositData, ); - execCalldata = routerIface.encodeFunctionData('performExecution', [ - execPayload, - ]); + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(execPayload)); } await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); @@ -205,27 +205,29 @@ function buildMonolithicExecutionUsdcPolygonToBase( relaySpender: string, depositTarget: string, depositData: string, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { - user: signerAddress, - inputToken: TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - }, - preFee: { - receiver: signerAddress, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: depositTarget, - approvalSpender: relaySpender, - value: 0n, - data: depositData, - amountPositions: [], - useFinalAmountAsValue: false, + exec: { + input: { + user: signerAddress, + inputToken: TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + }, + preFee: { + receiver: signerAddress, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: depositTarget, + approvalSpender: relaySpender, + value: 0n, + }, + flags: 0n, }, + swapCallData: '0x', + bridgeCallData: depositData, }; } @@ -319,9 +321,7 @@ async function executeLegUsdcPolygonToBase(args: { depositTarget, depositData, ); - execCalldata = routerIface.encodeFunctionData('performExecution', [ - execPayload, - ]); + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(execPayload)); } await ensureAllowanceForAllowanceHolder( diff --git a/scripts/e2e/bridgeViaRelaySimple.ts b/scripts/e2e/bridgeViaRelaySimple.ts index 1077fbf..3577866 100644 --- a/scripts/e2e/bridgeViaRelaySimple.ts +++ b/scripts/e2e/bridgeViaRelaySimple.ts @@ -1,13 +1,12 @@ /** * Script — Bridge AAVE (Polygon PoS) → AAVE (Base) via Relay.link - * using the `bridge(InputData, bytes feeBytes, BridgeData)` entrypoint. + * using the `bridge(InputData, FeeData, BridgeData, bytes)` entrypoint. * * Flow: * 1. Read signer's Polygon AAVE (or USDC) balance. - * 2. Compute fee via FEE_BPS; encode as 64-byte ABI word-pair (receiver, amount). - * If FEE_AMOUNT_BPS=0, feeBytes is `0x` and the contract skips the fee entirely. + * 2. Compute fee via FEE_BPS; set fee.amount=0 to skip the fee entirely. * 3. Fetch Relay.link /quote/v2 for the net bridge amount (inputAmount − fee). - * 4. AllowanceHolder.exec → router.bridge(input, feeBytes, bridgeData). + * 4. AllowanceHolder.exec → router.bridge(input, fee, bridgeData, bridgeCallData). * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts @@ -36,7 +35,7 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; -import type { InputData, StaticBridgeData } from './utils/contractTypes'; +import type { BridgeData, FeeData, InputData } from './utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; import { logTxnSummary } from './utils/txnLogSummary'; import { @@ -46,30 +45,13 @@ import { const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); -// ─── feeBytes encoding ──────────────────────────────────────────────────────── - -/** - * Encode fee as the 64-byte ABI word-pair expected by the contract: - * abi.encode(address receiver, uint256 amount) - * Returns `'0x'` when feeAmount is zero so the contract skips the transfer. - */ -function encodeFeeBytes(receiver: string, feeAmount: bigint): string { - if (feeAmount === 0n) { - return '0x'; - } - return ethers.AbiCoder.defaultAbiCoder().encode( - ['address', 'uint256'], - [receiver, feeAmount], - ); -} - // ─── Execution builder ──────────────────────────────────────────────────────── interface BridgeParams { signerAddress: string; inputToken: string; inputAmount: bigint; - feeBytes: string; + fee: FeeData; relaySpender: string; depositTarget: string; /** Bridge calldata with finalAmount (= inputAmount - feeAmount) already encoded inside. */ @@ -83,16 +65,15 @@ function buildBridgeCalldata(routerIface: ethers.Interface, p: BridgeParams): st inputAmount: p.inputAmount, }; - // No amountPositions or useFinalAmountAsValue — the caller bakes the net amount + // No bridge amount-position flag or bridge-value flag — the caller bakes the net amount // directly into depositData before calling. - const bridgeData: StaticBridgeData = { + const bridgeData: BridgeData = { target: p.depositTarget, approvalSpender: p.relaySpender, value: 0n, - data: p.depositData, }; - return routerIface.encodeFunctionData('bridge', [input, p.feeBytes, bridgeData]); + return routerIface.encodeFunctionData('bridge', [input, p.fee, bridgeData, p.depositData]); } // ─── Execution leg ──────────────────────────────────────────────────────────── @@ -126,9 +107,8 @@ async function executeLeg(args: { console.log(`Fee (${FEE_BPS} bps): ${fmt(feeAmount)} ${config.symbol}`); console.log(`Bridge amount: ${fmt(bridgeAmount)} ${config.symbol}`); - // feeBytes: `0x` if no fee, else abi-encoded (receiver=signer, amount) - const feeBytes = encodeFeeBytes(signerAddress, feeAmount); - console.log(`feeBytes: ${feeBytes === '0x' ? '0x (no fee)' : `${feeBytes.slice(0, 18)}… (${feeBytes.length / 2 - 1} bytes)`}`); + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + console.log(`Fee tuple: ${fee.amount === 0n ? 'amount=0 (no fee)' : `receiver=${fee.receiver}, amount=${fee.amount}`}`); console.log('Fetching Relay.link quote...'); const quote = await fetchRelayQuoteV2({ @@ -151,7 +131,7 @@ async function executeLeg(args: { signerAddress, inputToken: config.inputToken, inputAmount, - feeBytes, + fee, relaySpender, depositTarget, depositData, diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index 30ac4a1..71c531b 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -12,7 +12,7 @@ * Monolithic mechanics: * - Pull inputAmount AAVE via AH.exec grant, approve OO router, swap AAVE → ETH. * - Post-swap fee (FEE_BPS) in ETH sent to signer. - * - useFinalAmountAsValue=true: router forwards actualFinalETH as msg.value to inbox. + * - BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to inbox. * - No ETH splice needed (depositEth takes no calldata amount param). * * Modular mechanics: @@ -62,7 +62,13 @@ import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { MonolithicExecution, NO_FEE, ZERO_ADDRESS } from './utils/contractTypes'; +import { + BRIDGE_VALUE_FLAG, + MonolithicExecutionCall, + NO_FEE, + ZERO_ADDRESS, + monolithicArgs, +} from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; import { @@ -205,7 +211,7 @@ function buildDepositEthCalldata(): string { /** * AAVE → OO → ETH → Arbitrum inbox (monolithic): * - input: AAVE pulled via AH - * - swap: AAVE → native ETH, useFinalAmountAsValue=true forwards actualFinalETH + * - swap: AAVE → native ETH, BRIDGE_VALUE_FLAG forwards actualFinalETH * - bridge: depositEth() — no amount in calldata, all ETH passed as msg.value */ function buildMonolithic( @@ -215,28 +221,29 @@ function buildMonolithic( minAmountOut: bigint, ooRouter: string, swapData: string, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: NATIVE_TOKEN_ADDRESS, - value: 0n, - minOutput: minAmountOut, - data: swapData, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { - target: ARBITRUM_INBOX, - approvalSpender: ZERO_ADDRESS, - value: 0n, // ignored — useFinalAmountAsValue=true - data: buildDepositEthCalldata(), - amountPositions: [], // no amount in calldata - useFinalAmountAsValue: true, + exec: { + input: { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { + target: ARBITRUM_INBOX, + approvalSpender: ZERO_ADDRESS, + value: 0n, // ignored when BRIDGE_VALUE_FLAG is set + }, + flags: BRIDGE_VALUE_FLAG, }, + swapCallData: swapData, + bridgeCallData: buildDepositEthCalldata(), }; } @@ -339,7 +346,7 @@ async function executeLeg( execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); } else { const mono = buildMonolithic(signerAddress, inputAmount, feeAmount, minAmountOut, ooRouter, swapData); - execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono)); } // Input is AAVE (ERC-20) — msg.value is always 0; ETH comes from the swap output. diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index e1afc98..22a734a 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -35,9 +35,11 @@ import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; import { - MonolithicExecution, + MonolithicExecutionCall, NO_FEE, NO_SWAP, + bridgeAmountPositionFlag, + monolithicArgs, } from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; @@ -128,35 +130,36 @@ function buildMonolithicExecution( swapData: string, depositForBurnData: string, tokenMessenger: string, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { - user: signerAddress, - inputToken: TOKENS.AAVE_POLYGON, - inputAmount, - }, - preFee: NO_FEE, - swap: { - target: ooRouterAddress, - approvalSpender: ooRouterAddress, - outputToken: TOKENS.USDC_POLYGON_CIRCLE, - value: 0n, - minOutput: minAmountOut, - data: swapData, - returnDataWordOffset: 0n, - }, - postFee: { - receiver: signerAddress, - amount: feeAmount, - }, - bridge: { - target: tokenMessenger, - approvalSpender: tokenMessenger, - value: 0n, - data: depositForBurnData, - amountPositions: [4n], - useFinalAmountAsValue: false, + exec: { + input: { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount, + }, + preFee: NO_FEE, + swap: { + target: ooRouterAddress, + approvalSpender: ooRouterAddress, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { + receiver: signerAddress, + amount: feeAmount, + }, + bridge: { + target: tokenMessenger, + approvalSpender: tokenMessenger, + value: 0n, + }, + flags: bridgeAmountPositionFlag(4n), }, + swapCallData: swapData, + bridgeCallData: depositForBurnData, }; } @@ -197,27 +200,29 @@ function buildMonolithicExecutionUsdcPolygonToBaseCctp( feeAmount: bigint, depositForBurnData: string, tokenMessenger: string, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { - user: signerAddress, - inputToken: TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - }, - preFee: { - receiver: signerAddress, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: tokenMessenger, - approvalSpender: tokenMessenger, - value: 0n, - data: depositForBurnData, - amountPositions: [4n], - useFinalAmountAsValue: false, + exec: { + input: { + user: signerAddress, + inputToken: TOKENS.USDC_POLYGON_CIRCLE, + inputAmount, + }, + preFee: { + receiver: signerAddress, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: tokenMessenger, + approvalSpender: tokenMessenger, + value: 0n, + }, + flags: bridgeAmountPositionFlag(4n), }, + swapCallData: '0x', + bridgeCallData: depositForBurnData, }; } @@ -291,7 +296,7 @@ async function executeLegUsdcPolygonToBaseCctp(args: { ), ]); } else { - execCalldata = routerIface.encodeFunctionData('performExecution', [ + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( buildMonolithicExecutionUsdcPolygonToBaseCctp( signerAddress, inputAmount, @@ -299,7 +304,7 @@ async function executeLegUsdcPolygonToBaseCctp(args: { depositForBurnData, polyCctp.tokenMessenger, ), - ]); + )); } await ensureAllowanceForAllowanceHolder( @@ -380,7 +385,7 @@ async function executeLeg(args: { ), ]); } else { - execCalldata = routerIface.encodeFunctionData('performExecution', [ + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( buildMonolithicExecution( signerAddress, inputAmount, @@ -391,7 +396,7 @@ async function executeLeg(args: { depositForBurnData, polyCctp.tokenMessenger, ), - ]); + )); } await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); diff --git a/scripts/e2e/swapBridgeViaCctpSimple.ts b/scripts/e2e/swapBridgeViaCctpSimple.ts index 46ae4f6..d5831e5 100644 --- a/scripts/e2e/swapBridgeViaCctpSimple.ts +++ b/scripts/e2e/swapBridgeViaCctpSimple.ts @@ -33,7 +33,7 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; -import type { InputData, StaticBridgeData } from './utils/contractTypes'; +import type { BridgeData, FeeData, InputData } from './utils/contractTypes'; import { logTxnSummary } from './utils/txnLogSummary'; import { ensureRouterErc20Balance, @@ -42,16 +42,6 @@ import { const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); -function encodeFeeBytes(receiver: string, feeAmount: bigint): string { - if (feeAmount === 0n) { - return '0x'; - } - return ethers.AbiCoder.defaultAbiCoder().encode( - ['address', 'uint256'], - [receiver, feeAmount], - ); -} - /** * CCTP `depositForBurn` with explicit burn amount (net after optional fee). */ @@ -87,7 +77,7 @@ function buildBridgeCalldata( signerAddress: string; inputToken: string; inputAmount: bigint; - feeBytes: string; + fee: FeeData; tokenMessenger: string; depositData: string; }, @@ -98,17 +88,17 @@ function buildBridgeCalldata( inputAmount: args.inputAmount, }; - const bridgeData: StaticBridgeData = { + const bridgeData: BridgeData = { target: args.tokenMessenger, approvalSpender: args.tokenMessenger, value: 0n, - data: args.depositData, }; return routerIface.encodeFunctionData('bridge', [ input, - args.feeBytes, + args.fee, bridgeData, + args.depositData, ]); } @@ -157,13 +147,13 @@ async function main(): Promise { true, ); - const feeBytes = encodeFeeBytes(signerAddress, feeAmount); + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; const routerIface = new ethers.Interface(ROUTER_ABI); const execCalldata = buildBridgeCalldata(routerIface, { signerAddress, inputToken, inputAmount, - feeBytes, + fee, tokenMessenger: polyCctp.tokenMessenger, depositData, }); diff --git a/scripts/e2e/swapBridgeViaOft.ts b/scripts/e2e/swapBridgeViaOft.ts index 0bc8725..5259910 100644 --- a/scripts/e2e/swapBridgeViaOft.ts +++ b/scripts/e2e/swapBridgeViaOft.ts @@ -68,7 +68,13 @@ import { import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { MonolithicExecution, NO_FEE, NO_SWAP } from './utils/contractTypes'; +import { + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + bridgeAmountPositionFlag, + monolithicArgs, +} from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; import { @@ -234,7 +240,7 @@ function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { * - Swap AAVE → USDT0 via OpenOcean (swap step) * - Post-swap fee: FEE_BPS of estimated USDT0 output transferred to signer * - Bridge remaining USDT0 via OFT Adapter (approval required) - * - useFinalAmountAsValue=false; amountPositions=[196n] splices actual balance into amountLD + * - bridge amount position flag splices actual balance into amountLD at byte 196 * - bridge.value = nativeFeeWithBuffer (forwarded as LZ msg.value) */ function buildCase1Monolithic( @@ -246,35 +252,36 @@ function buildCase1Monolithic( swapData: string, oftSendData: string, nativeFeeWithBuffer: bigint, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { - user: signer, - inputToken: TOKENS.AAVE_POLYGON, - inputAmount, - }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: TOKENS.USDT0_POLYGON, - value: 0n, - minOutput: minAmountOut, - data: swapData, - returnDataWordOffset: 0n, - }, - postFee: { - receiver: signer, - amount: feeAmount, - }, - bridge: { - target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, // adapter needs ERC-20 approval - value: nativeFeeWithBuffer, // forwarded as LZ native fee - data: oftSendData, - amountPositions: [BigInt(OFT_AMOUNT_LD_OFFSET)], // splice actual USDT0 balance at byte 196 - useFinalAmountAsValue: false, + exec: { + input: { + user: signer, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount, + }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { + receiver: signer, + amount: feeAmount, + }, + bridge: { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, // adapter needs ERC-20 approval + value: nativeFeeWithBuffer, // forwarded as LZ native fee + }, + flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), }, + swapCallData: swapData, + bridgeCallData: oftSendData, }; } @@ -397,7 +404,7 @@ async function executeCase1Leg(args: { ), ]); } else { - execCalldata = routerIface.encodeFunctionData('performExecution', [ + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( buildCase1Monolithic( signerAddress, inputAmount, @@ -408,7 +415,7 @@ async function executeCase1Leg(args: { oftSendData, nativeFeeWithBuffer, ), - ]); + )); } await ensureAllowanceForAllowanceHolder( @@ -448,7 +455,7 @@ async function executeCase1Leg(args: { * - No swap (NO_SWAP) * - Pre-bridge fee: FEE_BPS of input USDT0 transferred to signer * - Bridge remaining USDT0 via OFT Adapter (approval required) - * - useFinalAmountAsValue=false; amountPositions=[196n] splices actual balance + * - bridge amount position flag splices actual balance at byte 196 * - bridge.value = nativeFeeWithBuffer (forwarded as LZ msg.value) */ function buildCase2Monolithic( @@ -457,27 +464,29 @@ function buildCase2Monolithic( feeAmount: bigint, oftSendData: string, nativeFeeWithBuffer: bigint, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { - user: signer, - inputToken: TOKENS.USDT0_POLYGON, - inputAmount, - }, - preFee: { - receiver: signer, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, - value: nativeFeeWithBuffer, - data: oftSendData, - amountPositions: [BigInt(OFT_AMOUNT_LD_OFFSET)], - useFinalAmountAsValue: false, + exec: { + input: { + user: signer, + inputToken: TOKENS.USDT0_POLYGON, + inputAmount, + }, + preFee: { + receiver: signer, + amount: feeAmount, + }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }, + flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), }, + swapCallData: '0x', + bridgeCallData: oftSendData, }; } @@ -584,7 +593,7 @@ async function executeCase2Leg(args: { ), ]); } else { - execCalldata = routerIface.encodeFunctionData('performExecution', [ + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( buildCase2Monolithic( signerAddress, inputAmount, @@ -592,7 +601,7 @@ async function executeCase2Leg(args: { oftSendData, nativeFeeWithBuffer, ), - ]); + )); } await ensureAllowanceForAllowanceHolder( diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index 19384bf..18cedfe 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -9,15 +9,15 @@ * * Native-pool mechanics (cases 1 & 3): * send() requires msg.value >= amountLD + nativeFee (StargatePoolNative._assertMessagingFee). - * Monolithic: useFinalAmountAsValue=true (router forwards actualFinalAmount as msg.value). - * amountLD = minAmountOut - fee - nativeFeeWithBuffer; positions=[]. + * Monolithic: BRIDGE_VALUE_FLAG set (router forwards actualFinalAmount as msg.value). + * amountLD = minAmountOut - fee - nativeFeeWithBuffer; no splice flag. * Since actual >= min (OO slippage), msg.value >= amountLD + nativeFeeWithBuffer ✓ * Modular: amountLD = minAmountOut - fee - nativeFeeWithBuffer (same). * nativeCall Stargate with value = amountLD + nativeFeeWithBuffer = minAmountOut - fee. * * ERC20-pool mechanics (case 2): * send() uses ERC20 transferFrom for USDC; msg.value = nativeFee only. - * Monolithic: useFinalAmountAsValue=false, amountPositions=[196n], bridge.value=nativeFeeWithBuffer. + * Monolithic: bridge amount position flag set to 196, bridge.value=nativeFeeWithBuffer. * Modular: staticCall USDC.balanceOf(router) → spliceWord(196n) into Stargate calldata. * nativeCall Stargate with value = nativeFeeWithBuffer. * @@ -88,7 +88,15 @@ import { import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { MonolithicExecution, NO_FEE, NO_SWAP, ZERO_ADDRESS } from './utils/contractTypes'; +import { + BRIDGE_VALUE_FLAG, + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + ZERO_ADDRESS, + bridgeAmountPositionFlag, + monolithicArgs, +} from './utils/contractTypes'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; import { @@ -475,8 +483,8 @@ function buildStargateCalldata( /** * Monolithic for native-pool cases (cases 1 & 3): * - OO swap input token → native ETH - * - useFinalAmountAsValue=true: router forwards actualFinalETH as msg.value to Stargate - * - amountLD = minAmountOut - fee - nativeFeeWithBuffer; pre-encoded; no splice needed (positions=[]) + * - BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to Stargate + * - amountLD = minAmountOut - fee - nativeFeeWithBuffer; pre-encoded; no splice flag needed * - StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= min */ function buildNativePoolMonolithic( @@ -488,36 +496,37 @@ function buildNativePoolMonolithic( ooRouter: string, swapData: string, stargateData: string, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { user: signer, inputToken: cfg.inputToken, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: NATIVE_TOKEN_ADDRESS, - value: 0n, - minOutput: minAmountOut, - data: swapData, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signer, amount: feeAmount }, - bridge: { - target: cfg.bridgeContract, - approvalSpender: ZERO_ADDRESS, // no ERC20 approval for native ETH - value: 0n, // ignored when useFinalAmountAsValue=true - data: stargateData, - amountPositions: [], // amountLD is pre-encoded - useFinalAmountAsValue: true, // forward actualFinalETH as msg.value + exec: { + input: { user: signer, inputToken: cfg.inputToken, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signer, amount: feeAmount }, + bridge: { + target: cfg.bridgeContract, + approvalSpender: ZERO_ADDRESS, // no ERC20 approval for native ETH + value: 0n, // ignored when BRIDGE_VALUE_FLAG is set + }, + flags: BRIDGE_VALUE_FLAG, }, + swapCallData: swapData, + bridgeCallData: stargateData, }; } /** * Monolithic for ERC20-pool case (case 2): * - No OO swap (NO_SWAP) — input USDC goes directly to bridge - * - useFinalAmountAsValue=false: USDC transferred via ERC20 approval - * - amountPositions=[196n]: router splices finalAmount into amountLD at runtime + * - USDC transferred via ERC20 approval + * - bridge amount position flag set to 196: router splices finalAmount into amountLD at runtime * - bridge.value=nativeFeeWithBuffer: forwarded as msg.value for the LZ fee */ function buildErc20PoolMonolithic( @@ -527,20 +536,22 @@ function buildErc20PoolMonolithic( feeAmount: bigint, stargateData: string, nativeFeeWithBuffer: bigint, -): MonolithicExecution { +): MonolithicExecutionCall { return { - input: { user: signer, inputToken: cfg.inputToken, inputAmount }, - preFee: NO_FEE, - swap: NO_SWAP, // skip swap — finalToken = inputToken, finalAmount = inputAmount - preFee - postFee: { receiver: signer, amount: feeAmount }, - bridge: { - target: cfg.bridgeContract, - approvalSpender: cfg.bridgeContract, // router must approve USDC to pool - value: nativeFeeWithBuffer, // POL/native forwarded as LZ fee msg.value - data: stargateData, - amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice at byte 196 - useFinalAmountAsValue: false, + exec: { + input: { user: signer, inputToken: cfg.inputToken, inputAmount }, + preFee: NO_FEE, + swap: NO_SWAP, // skip swap — finalToken = inputToken, finalAmount = inputAmount - preFee + postFee: { receiver: signer, amount: feeAmount }, + bridge: { + target: cfg.bridgeContract, + approvalSpender: cfg.bridgeContract, // router must approve USDC to pool + value: nativeFeeWithBuffer, // POL/native forwarded as LZ fee msg.value + }, + flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), }, + swapCallData: '0x', + bridgeCallData: stargateData, }; } @@ -665,30 +676,31 @@ function buildNativeInErc20BridgeMonolithic( stargateData: string, nativeFeeWithBuffer: bigint, ooSwapNativeWei: bigint, -): MonolithicExecution { +): MonolithicExecutionCall { const rawOoWei = ooSwapNativeWei > 0n ? ooSwapNativeWei : inputAmount; const polOrEthToOo = rawOoWei <= inputAmount ? rawOoWei : inputAmount; return { - input: { user: signer, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ZERO_ADDRESS, // no ERC20 approve for native ETH input - outputToken: cfg.ooSwap!.outToken, - value: polOrEthToOo, - minOutput: minAmountOut, - data: swapData, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signer, amount: feeAmount }, // fee in OO output token (USDC/USDT0) - bridge: { - target: cfg.bridgeContract, - approvalSpender: cfg.bridgeContract, // router approves bridge contract to pull ERC20 - value: nativeFeeWithBuffer, // LZ fee in native gas token only - data: stargateData, - amountPositions: [BigInt(STARGATE_AMOUNT_LD_OFFSET)], // splice amountLD at runtime - useFinalAmountAsValue: false, + exec: { + input: { user: signer, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ZERO_ADDRESS, // no ERC20 approve for native ETH input + outputToken: cfg.ooSwap!.outToken, + value: polOrEthToOo, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signer, amount: feeAmount }, // fee in OO output token (USDC/USDT0) + bridge: { + target: cfg.bridgeContract, + approvalSpender: cfg.bridgeContract, // router approves bridge contract to pull ERC20 + value: nativeFeeWithBuffer, // LZ fee in native gas token only + }, + flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), }, + swapCallData: swapData, + bridgeCallData: stargateData, }; } @@ -963,7 +975,7 @@ async function executeLeg( } execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); } else { - let mono: MonolithicExecution; + let mono: MonolithicExecutionCall; if (cfg.isNativePool) { mono = buildNativePoolMonolithic( signerAddress, cfg, inputAmountWei, feeAmount, minAmountOut, @@ -979,7 +991,7 @@ async function executeLeg( signerAddress, cfg, inputAmountWei, feeAmount, stargateData, nativeFeeWithBuffer, ); } - execCalldata = routerIface.encodeFunctionData('performExecution', [mono]); + execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono)); } const txValue = cfg.isNativeInput ? inputAmountWei + nativeFeeWithBuffer : nativeFeeWithBuffer; diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index ff9b5b6..2721773 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -23,7 +23,6 @@ export interface SwapData { outputToken: string; value: bigint; minOutput: bigint; - data: string; returnDataWordOffset: bigint; } @@ -31,17 +30,6 @@ export interface BridgeData { target: string; approvalSpender: string; value: bigint; - data: string; - amountPositions: bigint[]; - useFinalAmountAsValue: boolean; -} - -/** Simplified bridge descriptor for the no-swap `bridge()` entrypoint. */ -export interface StaticBridgeData { - target: string; - approvalSpender: string; - value: bigint; - data: string; } export interface MonolithicExecution { @@ -50,6 +38,34 @@ export interface MonolithicExecution { swap: SwapData; postFee: FeeData; bridge: BridgeData; + flags: bigint; +} + +export interface MonolithicExecutionCall { + exec: MonolithicExecution; + swapCallData: string; + bridgeCallData: string; +} + +export const BRIDGE_VALUE_FLAG = 4n; +export const BRIDGE_AMOUNT_POSITION_FLAG = 8n; +export const BRIDGE_AMOUNT_POSITION_SHIFT = 16n; +export const MAX_BRIDGE_AMOUNT_POSITION = 0xffffn; + +export function bridgeAmountPositionFlag(position: bigint | number): bigint { + const positionBigInt = BigInt(position); + if (positionBigInt < 0n || positionBigInt > MAX_BRIDGE_AMOUNT_POSITION) { + throw new Error(`bridge amount position exceeds uint16: ${positionBigInt}`); + } + return BRIDGE_AMOUNT_POSITION_FLAG | (positionBigInt << BRIDGE_AMOUNT_POSITION_SHIFT); +} + +export function monolithicArgs(call: MonolithicExecutionCall) { + return [ + call.exec, + call.swapCallData, + call.bridgeCallData, + ] as const; } // ─── Sentinel / zero helpers ────────────────────────────────────────────────── @@ -66,6 +82,5 @@ export const NO_SWAP: SwapData = { outputToken: ZERO_ADDRESS, value: 0n, minOutput: 0n, - data: '0x', returnDataWordOffset: 0n, }; diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index f2dae81..9494de6 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -8,10 +8,13 @@ export const ROUTER_ABI = [ ( (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) preFee, - (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, bytes data, uint256 returnDataWordOffset) swap, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swap, (address receiver, uint256 amount) postFee, - (address target, address approvalSpender, uint256 value, bytes data, uint256[] amountPositions, bool useFinalAmountAsValue) bridge - ) exec + (address target, address approvalSpender, uint256 value) bridge, + uint256 flags + ) exec, + bytes swapCallData, + bytes bridgeCallData ) external payable`, // Modular path @@ -22,7 +25,8 @@ export const ROUTER_ABI = [ // Simple bridge path (no swap, no splicing — caller pre-encodes finalAmount into data) `function bridge( (address user, address inputToken, uint256 inputAmount) input, - bytes feeBytes, - (address target, address approvalSpender, uint256 value, bytes data) bridgeData + (address receiver, uint256 amount) fee, + (address target, address approvalSpender, uint256 value) bridgeData, + bytes bridgeCallData ) external payable`, ] as const; diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index d4bcaf5..b5685f5 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -53,7 +53,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { address outputToken; uint256 value; uint256 minOutput; - bytes data; uint256 returnDataWordOffset; } @@ -61,22 +60,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { address target; address approvalSpender; uint256 value; - bytes data; - uint256[] amountPositions; - // when true, bridge.value is ignored and finalAmount is forwarded as - // msg.value instead — needed for native-token bridges (e.g. Arbitrum inbox) - // where the bridged amount is only known at runtime. - bool useFinalAmountAsValue; - } - - /// @dev Simplified bridge descriptor for the no-swap `bridge()` path. - /// The caller knows `finalAmount = inputAmount - feeAmount` before encoding, - /// so no amount-splicing or runtime value overrides are needed. - struct StaticBridgeData { - address target; - address approvalSpender; - uint256 value; - bytes data; } struct MonolithicExecution { @@ -85,9 +68,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { SwapData swap; FeeData postFee; BridgeData bridge; - /// Packed byte; monolithic pipeline only tests `BALANCE_FLAG_BIT_MASK` (bit 1) in `_execSwap`. + /// Packed flags; monolithic pipeline tests `BALANCE_FLAG_BIT_MASK` in `_execSwap` + /// and `BRIDGE_VALUE_FLAG_BIT_MASK` in `_doBridge`. /// Fee timing uses `preFee` / `postFee` structs — `FEE_FLAG_BIT_MASK` (bit 0) is ignored here. - uint8 flags; + uint256 flags; } // ========================================================================= @@ -110,24 +94,29 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Flags (swap / swapAndBridge / monolithic swap step) // ========================================================================= // - // Instead of two bool parameters, one uint8 packs independent switches; future flags can use - // bit 2 (0x04), bit 3 (0x08), etc. without changing the ABI shape. + // Instead of bool parameters, one uint256 packs independent switches without adding + // ABI range checks or extra words for standalone bools. // // Bit layout (least significant bits); test with `(flags & MASK) != 0`: - // bits 7..2 : reserved (0) + // bits 255..32 : reserved (0) + // bits 31..16 : bridge amount word byte offset, uint16, used only when bit 3 is set + // bits 15..4 : reserved (0) + // bit 3 : BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK (0x08) — splice finalAmount into bridge calldata + // bit 2 : BRIDGE_VALUE_FLAG_BIT_MASK (0x04) — bridge msg.value: static value vs final amount // bit 1 : BALANCE_FLAG_BIT_MASK (0x02) — swap output: returndata vs balance delta // bit 0 : FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap (standalone paths only) // // Combined values for swap()/swapAndBridge(): // - // flags binary (low byte) postFee? balance-of output? - // ───── ────────────────── ──────── ────────────────── - // 0x00 00000000 no returndata word - // 0x01 00000001 yes returndata word - // 0x02 00000010 no balance delta on outputToken - // 0x03 00000011 yes balance delta on outputToken + // flags binary (low byte) postFee? balance-of output? bridge value? + // ───── ────────────────── ──────── ────────────────── ───────────── + // 0x00 00000000 no returndata word bridge.value + // 0x01 00000001 yes returndata word bridge.value + // 0x02 00000010 no balance delta on outputToken bridge.value + // 0x03 00000011 yes balance delta on outputToken bridge.value + // 0x04 00000100 no returndata word finalAmount // - // FEE_FLAG_BIT_MASK selects bit 0 — fee timing (see `_collectFee` + swap flow). + // FEE_FLAG_BIT_MASK selects bit 0 — fee timing. // Cleared — pull → deduct fee from input token → swap remainder → standalone swap skips minOutput. // Set — pull → swap full input → deduct fee from output token → standalone swap checks minOutput. // @@ -135,13 +124,33 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Cleared — decode returned amount from call returndata at `swapData.returnDataWordOffset`. // Set — snapshot outputToken balance before call, measure (after − before) as output. // - // Monolithic `performExecution` applies only `BALANCE_FLAG_BIT_MASK` in `_execSwap`; fee timing is `preFee`/`postFee` structs. + // BRIDGE_VALUE_FLAG_BIT_MASK selects bit 2 — bridge native value source. + // Cleared — forward `bridge.value`. + // Set — forward finalAmount as msg.value. + // + // BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK selects bit 3 — bridge calldata amount splicing. + // Cleared — no runtime amount splice. + // Set — splice finalAmount at uint16(flags >> BRIDGE_AMOUNT_POSITION_SHIFT). + // + // Monolithic `performExecution` ignores `FEE_FLAG_BIT_MASK`; fee timing is `preFee`/`postFee` structs. /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. - uint8 internal constant FEE_FLAG_BIT_MASK = 0x01; + uint256 internal constant FEE_FLAG_BIT_MASK = 0x01; /// @dev Bit mask 0x02: measure swap output by balance delta when `(flags & mask) != 0`; clear = returndata word. - uint8 internal constant BALANCE_FLAG_BIT_MASK = 0x02; + uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; + + /// @dev Bit mask 0x04: bridge.value is ignored and finalAmount is forwarded as msg.value. + uint256 internal constant BRIDGE_VALUE_FLAG_BIT_MASK = 0x04; + + /// @dev Bit mask 0x08: splice finalAmount into bridge calldata at the uint16 position packed in flags. + uint256 internal constant BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK = 0x08; + + /// @dev Shift for the packed uint16 bridge amount position. + uint256 internal constant BRIDGE_AMOUNT_POSITION_SHIFT = 16; + + /// @dev Mask for the packed uint16 bridge amount position after shifting. + uint256 internal constant BRIDGE_AMOUNT_POSITION_MASK = 0xffff; // ========================================================================= // Errors @@ -173,15 +182,20 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Executes the monolithic pipeline without signature verification: * pull via AH, optional pre-swap fee, optional swap, optional - * post-swap fee, bridge call with multi-position amount splicing. + * post-swap fee, bridge call with optional single-position amount splicing. * @dev The caller MUST route through `AllowanceHolder.exec` so that * `_msgSender()` resolves to `exec.input.user`. There is no nonce or * deadline; replay protection is the caller's responsibility. * Bit 0 (`FEE_FLAG_BIT_MASK`) is unused in monolithic runs; fee placement is `preFee` / `postFee` structs. - * `exec.flags` only contributes `BALANCE_FLAG_BIT_MASK` to the optional `_execSwap` step. + * `exec.flags` contributes `BALANCE_FLAG_BIT_MASK` to `_execSwap` and + * `BRIDGE_VALUE_FLAG_BIT_MASK` to bridge msg.value selection. */ - function performExecution(MonolithicExecution calldata exec) external payable { - _runMonolithic(exec); + function performExecution( + MonolithicExecution calldata exec, + bytes calldata swapCallData, + bytes calldata bridgeCallData + ) external payable { + _runMonolithic(exec, swapCallData, bridgeCallData); } // ========================================================================= @@ -190,10 +204,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre/post fee → swap. - * @param flags Packed `uint8`; OR masks then test with `flags & MASK != 0`. Masks: `FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` (0x02). + * @param flags Packed flags; OR masks then test with `flags & MASK != 0`. Masks: `FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` (0x02). * Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), * `2` (pre-fee, balance delta), `3` (post-fee, balance delta). - * @param feeBytes 0x = no fee; 64 bytes = abi.encode(address receiver, uint256 amount). + * @param fee Set `amount` to 0 to skip fee collection. * @dev minOutput is only enforced in post-fee mode. Pre-fee skips minOutput check. * Post-fee: fee collected from output token after swap, then minOutput validated. * Pre-fee: fee collected from input token before swap, minOutput skipped. @@ -201,9 +215,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { */ function swap( InputData calldata input, - uint8 flags, - bytes calldata feeBytes, - SwapData calldata swapData + uint256 flags, + FeeData calldata fee, + SwapData calldata swapData, + bytes calldata swapCallData ) external payable returns (uint256 finalAmount) { if (input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0)) { revert InvalidExecution(); @@ -211,17 +226,20 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { _pullFromUser(input.inputToken, input.user, input.inputAmount); - // Check feeBytes first: flag bit is only read when a fee is actually present. + // Check fee amount first: flag bit is only read when a fee is actually present. // FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output - bool hasFee = feeBytes.length != 0; + bool hasFee = fee.amount != 0; /// @dev if hasFee is false, we short-circuit and flag check wont execute at runtime saving gas bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; uint256 swapInput = input.inputAmount; if (hasFee && !postFee) { - uint256 fee = _collectFee(input.inputToken, feeBytes); - if (fee > swapInput) revert InsufficientFunds(); - unchecked { swapInput -= fee; } + uint256 feeAmount = fee.amount; + if (feeAmount > swapInput) revert InsufficientFunds(); + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + unchecked { + swapInput -= feeAmount; + } } if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { @@ -229,12 +247,15 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // BALANCE_FLAG_BIT_MASK: unset ⇒ decode output word from returndata; set ⇒ output = delta on outputToken - finalAmount = _execSwap(swapData, flags & BALANCE_FLAG_BIT_MASK != 0); + finalAmount = _execSwap(swapData, swapCallData, flags & BALANCE_FLAG_BIT_MASK != 0); if (postFee) { - uint256 fee = _collectFee(swapData.outputToken, feeBytes); - if (fee > finalAmount) revert InsufficientFunds(); - unchecked { finalAmount -= fee; } + uint256 feeAmount = fee.amount; + if (feeAmount > finalAmount) revert InsufficientFunds(); + CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); + unchecked { + finalAmount -= feeAmount; + } if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); } } @@ -245,58 +266,87 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre/post swap fee → swap → bridge with runtime amount splicing. - * @param flags Same packing as `swap`: 0x00–0x03 as documented on the flag constants block. - * @param feeBytes 0x = no fee; 64 bytes = abi.encode(address receiver, uint256 amount). + * @param flags Same packing as `swap`; additionally bit 2 forwards final amount as bridge msg.value. + * @param fee Set `amount` to 0 to skip fee collection. * @dev minOutput is always enforced (after fee deduction in post-fee mode). * Post-fee: fee collected from output token after swap, then minOutput validated. * Pre-fee: fee collected from input token before swap, minOutput still validated. */ function swapAndBridge( InputData calldata input, - uint8 flags, - bytes calldata feeBytes, + uint256 flags, + FeeData calldata fee, SwapData calldata swapData, - BridgeData calldata bridgeData + bytes calldata swapCallData, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData ) external payable { if ( - bridgeData.target == address(0) || input.user == address(0) || - input.inputToken == address(0) || swapData.target == address(0) + bridgeData.target == address(0) || input.user == address(0) || input.inputToken == address(0) + || swapData.target == address(0) ) { revert InvalidExecution(); } - _pullFromUser(input.inputToken, input.user, input.inputAmount); + uint256 finalAmount = _swapAndBridgeSwap(input, flags, fee, swapData, swapCallData); - // Check feeBytes first: flag bit is only read when a fee is actually present. - // FEE_FLAG_BIT_MASK: same semantics as standalone `swap` (fee from input vs output token) - bool hasFee = feeBytes.length != 0; - bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; - uint256 swapInput = input.inputAmount; + // Always check minOutput (unlike standalone swap where pre-fee skips this) + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); - if (hasFee && !postFee) { - uint256 fee = _collectFee(input.inputToken, feeBytes); - if (fee > swapInput) revert InsufficientFunds(); - unchecked { swapInput -= fee; } - } + _finishSwapAndBridge(swapData.outputToken, finalAmount, bridgeData, bridgeCallData, flags); + } - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + function _swapAndBridgeSwap( + InputData calldata input, + uint256 flags, + FeeData calldata fee, + SwapData calldata swapData, + bytes calldata swapCallData + ) internal returns (uint256 finalAmount) { + _pullFromUser(input.inputToken, input.user, input.inputAmount); + bool postFee; + { + // Check fee amount first: flag bit is only read when a fee is actually present. + // FEE_FLAG_BIT_MASK: same semantics as standalone `swap` (fee from input vs output token) + uint256 feeAmount = fee.amount; + postFee = feeAmount != 0 && flags & FEE_FLAG_BIT_MASK != 0; + uint256 swapInput = input.inputAmount; + + if (feeAmount != 0 && !postFee) { + if (feeAmount > swapInput) revert InsufficientFunds(); + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + unchecked { + swapInput -= feeAmount; + } + } + + if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + } } - address finalToken = swapData.outputToken; // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` - uint256 finalAmount = _execSwap(swapData, flags & BALANCE_FLAG_BIT_MASK != 0); + bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; + finalAmount = _execSwap(swapData, swapCallData, useBalanceOf); if (postFee) { - uint256 fee = _collectFee(finalToken, feeBytes); - if (fee > finalAmount) revert InsufficientFunds(); - unchecked { finalAmount -= fee; } + uint256 feeAmount = fee.amount; + if (feeAmount > finalAmount) revert InsufficientFunds(); + CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); + unchecked { + finalAmount -= feeAmount; + } } + } - // Always check minOutput (unlike standalone swap where pre-fee skips this) - if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); - - _doBridge(finalToken, finalAmount, bridgeData); + function _finishSwapAndBridge( + address finalToken, + uint256 finalAmount, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData, + uint256 flags + ) internal { + _doBridge(finalToken, finalAmount, bridgeData, bridgeCallData, flags); } // ========================================================================= @@ -305,50 +355,35 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre-bridge fee → bridge, with no swap step. - * @dev `feeBytes` must be either empty (`0x`, skip fee) or exactly 64 bytes - * ABI-encoded as `(address receiver, uint256 amount)`. Any other - * length reverts with `InvalidExecution`. - * - * Because no swap is involved, `finalAmount = inputAmount - feeAmount` is + * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is * fully knowable by the caller before signing. The caller must therefore - * bake the correct amount directly into `bridgeData.data` and set + * bake the correct amount directly into `bridgeCallData` and set * `bridgeData.value` to the desired `msg.value` for the bridge call. * No runtime calldata splicing is performed. * * The caller MUST route through `AllowanceHolder.exec` for ERC-20 * inputs so that `_msgSender()` resolves to `input.user`. */ - function bridge(InputData calldata input, bytes calldata feeBytes, StaticBridgeData calldata bridgeData) - external - payable - { + function bridge( + InputData calldata input, + FeeData calldata fee, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData + ) external payable { if (bridgeData.target == address(0) || input.user == address(0) || input.inputToken == address(0)) { revert InvalidExecution(); } - // feeBytes must be empty or exactly one ABI word-pair (address + uint256 = 64 bytes) - if (feeBytes.length != 0 && feeBytes.length != 64) { - revert InvalidExecution(); - } - // 1. pull funds from user via AllowanceHolder _pullFromUser(input.inputToken, input.user, input.inputAmount); - // 2. optional pre-bridge fee decoded from feeBytes; track net amount for approval - uint256 feeAmount; - if (feeBytes.length == 64) { - address feeReceiver; - assembly ("memory-safe") { - // feeBytes is a calldata slice: feeBytes.offset points at the raw bytes - feeReceiver := calldataload(feeBytes.offset) - feeAmount := calldataload(add(feeBytes.offset, 0x20)) - } - if (feeAmount != 0) { - if (feeAmount > input.inputAmount) { - revert InsufficientFunds(); - } - CurrencyLib.transfer(input.inputToken, feeReceiver, feeAmount); + // 2. optional pre-bridge fee; track net amount for approval + uint256 feeAmount = fee.amount; + if (feeAmount != 0) { + if (feeAmount > input.inputAmount) { + revert InsufficientFunds(); } + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); } // 3. optional approval to bridge spender for the net amount (inputAmount - feeAmount) @@ -361,8 +396,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // 4. bridge call — data and value are pre-encoded by the caller - bytes memory bData = bridgeData.data; - _doCall(bridgeData.target, bridgeData.value, bData, false); + _doCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); } // ========================================================================= @@ -381,7 +415,11 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Internal: monolithic pipeline // ========================================================================= - function _runMonolithic(MonolithicExecution calldata exec) internal { + function _runMonolithic( + MonolithicExecution calldata exec, + bytes calldata swapCallData, + bytes calldata bridgeCallData + ) internal { if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { revert InvalidExecution(); } @@ -398,7 +436,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { address finalToken; uint256 finalAmount; if (exec.swap.target != address(0)) { - (finalToken, finalAmount) = _performSwap(exec); + (finalToken, finalAmount) = _performSwap(exec, swapCallData); } else { if (exec.preFee.amount > exec.input.inputAmount) { revert InsufficientFunds(); @@ -421,21 +459,32 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // 5. bridge: splice, approve, call - _doBridge(finalToken, finalAmount, exec.bridge); + _finishMonolithicBridge(exec, finalToken, finalAmount, bridgeCallData); } - function _performSwap(MonolithicExecution calldata exec) + function _finishMonolithicBridge( + MonolithicExecution calldata exec, + address finalToken, + uint256 finalAmount, + bytes calldata bridgeCallData + ) internal { + _doBridge(finalToken, finalAmount, exec.bridge, bridgeCallData, exec.flags); + } + + function _performSwap(MonolithicExecution calldata exec, bytes calldata swapCallData) internal returns (address finalToken, uint256 finalAmount) { if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 swapInput; - unchecked { swapInput = exec.input.inputAmount - exec.preFee.amount; } + unchecked { + swapInput = exec.input.inputAmount - exec.preFee.amount; + } SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); } // Monolithic path: only `BALANCE_FLAG_BIT_MASK` is read for `_execSwap`; fee uses `preFee` / `postFee`, not bit 0. - finalAmount = _execSwap(exec.swap, exec.flags & BALANCE_FLAG_BIT_MASK != 0); + finalAmount = _execSwap(exec.swap, swapCallData, exec.flags & BALANCE_FLAG_BIT_MASK != 0); if (finalAmount < exec.swap.minOutput) revert SwapOutputInsufficient(); finalToken = exec.swap.outputToken; } @@ -447,48 +496,43 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /// @dev Execute swap; output measured via returndata word or output-token balance delta. /// useBalanceOf=true: measure output as (balance after - balance before). /// useBalanceOf=false: decode output from returndata at swapData.returnDataWordOffset. - function _execSwap(SwapData calldata swapData, bool useBalanceOf) internal returns (uint256 finalAmount) { + function _execSwap(SwapData calldata swapData, bytes calldata swapCallData, bool useBalanceOf) + internal + returns (uint256 finalAmount) + { if (useBalanceOf) { // Balance delta mode: snapshot before, call, measure delta uint256 before = CurrencyLib.balanceOf(swapData.outputToken, address(this)); - _doCall(swapData.target, swapData.value, swapData.data, false); + _doCallCalldata(swapData.target, swapData.value, swapCallData, false); finalAmount = CurrencyLib.balanceOf(swapData.outputToken, address(this)) - before; } else { // Returndata mode: decode output from a specific word in returndata - bytes memory ret = _doCall(swapData.target, swapData.value, swapData.data, true); + bytes memory ret = _doCallCalldata(swapData.target, swapData.value, swapCallData, true); finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); } } - /// @dev Decode, validate, and collect fee from feeBytes. Returns fee amount (0 if feeBytes empty). - /// feeBytes encoding: - /// - 0x (zero length): no fee, return 0 immediately. - /// - 64 bytes: abi.encode(address receiver, uint256 amount). Transfer amount to receiver. - /// Caller must pass the correct `token` address: - /// - Pre-fee: pass inputToken (fee deducted before swap). - /// - Post-fee: pass outputToken (fee deducted after swap). - function _collectFee(address token, bytes calldata feeBytes) internal returns (uint256 feeAmount) { - if (feeBytes.length != 64) revert InvalidExecution(); - address receiver; - assembly ("memory-safe") { - receiver := calldataload(feeBytes.offset) - feeAmount := calldataload(add(feeBytes.offset, 0x20)) - } - if (feeAmount != 0) CurrencyLib.transfer(token, receiver, feeAmount); - } - /// @dev Splice finalAmount into bridge calldata, approve, and call bridge target. - function _doBridge(address token, uint256 amount, BridgeData calldata bd) internal { - bytes memory bData = bd.data; - BytesSpliceLib.spliceWords({data: bData, positions: bd.amountPositions, word: amount}); + function _doBridge( + address token, + uint256 amount, + BridgeData calldata bd, + bytes calldata bridgeCallData, + uint256 flags + ) internal { + bytes memory bData = bridgeCallData; + if (flags & BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK != 0) { + uint256 position = flags >> BRIDGE_AMOUNT_POSITION_SHIFT & BRIDGE_AMOUNT_POSITION_MASK; + BytesSpliceLib.spliceWord({data: bData, position: position, word: amount}); + } if (bd.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(token, bd.approvalSpender, amount); } - // when useFinalAmountAsValue, forward amount as msg.value for native-token bridges - uint256 bridgeValue = bd.useFinalAmountAsValue ? amount : bd.value; - _doCall(bd.target, bridgeValue, bData, false); + // when set, forward amount as msg.value for native-token bridges + uint256 bridgeValue = flags & BRIDGE_VALUE_FLAG_BIT_MASK != 0 ? amount : bd.value; + _doCall(bd.target, bridgeValue, bData); } // ========================================================================= @@ -617,13 +661,35 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Internal: simple call dispatcher (used by monolithic path) // ========================================================================= - function _doCall(address target, uint256 value, bytes memory data, bool storeResult) + function _doCall(address target, uint256 value, bytes memory data) internal { + bool success; + assembly ("memory-safe") { + success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) + } + + if (!success) { + bytes memory ret; + assembly ("memory-safe") { + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) + returndatacopy(add(ret, 0x20), 0, returnDataSize) + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) + revert(add(ret, 0x20), mload(ret)) + } + } + } + + function _doCallCalldata(address target, uint256 value, bytes calldata data, bool storeResult) internal returns (bytes memory ret) { bool success; assembly ("memory-safe") { - success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) + let ptr := mload(0x40) + calldatacopy(ptr, data.offset, data.length) + mstore(0x40, and(add(add(ptr, data.length), 0x1f), not(0x1f))) + success := call(gas(), target, value, ptr, data.length, 0, 0) } if (!success || storeResult) { diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 0c24dff..6d5ee09 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -119,7 +119,7 @@ contract OneInchCctpOpenRouterPoCTest is Test { uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); - Router.MonolithicExecution memory exec = + (Router.MonolithicExecution memory exec, bytes memory swapCallData, bytes memory bridgeCallData) = _buildMonolithicExecution(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); vm.prank(FIXTURE_RECIPIENT); @@ -129,7 +129,7 @@ contract OneInchCctpOpenRouterPoCTest is Test { POLYGON_AAVE, inputAmount, payable(address(router)), - abi.encodeCall(router.performExecution, (exec)) + abi.encodeCall(router.performExecution, (exec, swapCallData, bridgeCallData)) ); uint256 executeGasUsed = gasBeforeExecute - gasleft(); emit log_named_uint("AllowanceHolder.exec -> router.performExecution gas used", executeGasUsed); @@ -239,10 +239,10 @@ contract OneInchCctpOpenRouterPoCTest is Test { function _buildMonolithicExecution(uint256 inputAmount, bytes memory swapCalldata) internal pure - returns (Router.MonolithicExecution memory exec) + returns (Router.MonolithicExecution memory exec, bytes memory swapCallData, bytes memory bridgeCallData) { - uint256[] memory amountPositions = new uint256[](1); - amountPositions[0] = 4; + swapCallData = swapCalldata; + bridgeCallData = _emptyDepositForBurnCalldata(); exec = Router.MonolithicExecution({ input: Router.InputData({user: FIXTURE_RECIPIENT, inputToken: POLYGON_AAVE, inputAmount: inputAmount}), @@ -253,18 +253,11 @@ contract OneInchCctpOpenRouterPoCTest is Test { outputToken: POLYGON_USDC, value: 0, minOutput: EXPECTED_SWAP_OUTPUT_USDC, - data: swapCalldata, returnDataWordOffset: 0 }), postFee: Router.FeeData({receiver: FEE_RECIPIENT, amount: ROUTE_FEE_USDC}), - bridge: Router.BridgeData({ - target: CCTP_TOKEN_MESSENGER_V2, - approvalSpender: CCTP_TOKEN_MESSENGER_V2, - value: 0, - data: _emptyDepositForBurnCalldata(), - amountPositions: amountPositions, - useFinalAmountAsValue: false - }) + bridge: Router.BridgeData({target: CCTP_TOKEN_MESSENGER_V2, approvalSpender: CCTP_TOKEN_MESSENGER_V2, value: 0}), + flags: 0x08 | (uint256(4) << 16) }); } From 82faec86732050ab3dc302ad6be970c7ecad36f5 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Fri, 15 May 2026 22:31:21 +0530 Subject: [PATCH 34/69] feat: requestHash event --- src/combined/BungeeOpenRouterV2Unchecked.sol | 27 +++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 1a984d0..729699c 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -167,6 +167,12 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { error MissingNativeValue(uint256 actionIndex); error ReturnDataOutOfBounds(); + // ========================================================================= + // Events + // ========================================================================= + + event RequestExecuted(bytes32 indexed requestHash); + // ========================================================================= // Constructor // ========================================================================= @@ -189,13 +195,16 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * Bit 0 (`FEE_FLAG_BIT_MASK`) is unused in monolithic runs; fee placement is `preFee` / `postFee` structs. * `exec.flags` contributes `BALANCE_FLAG_BIT_MASK` to `_execSwap` and * `BRIDGE_VALUE_FLAG_BIT_MASK` to bridge msg.value selection. + * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. */ function performExecution( + bytes32 requestHash, MonolithicExecution calldata exec, bytes calldata swapCallData, bytes calldata bridgeCallData ) external payable { _runMonolithic(exec, swapCallData, bridgeCallData); + emit RequestExecuted(requestHash); } // ========================================================================= @@ -204,6 +213,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre/post fee → swap. + * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. * @param flags Packed flags; OR masks then test with `flags & MASK != 0`. Masks: `FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` (0x02). * Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), * `2` (pre-fee, balance delta), `3` (post-fee, balance delta). @@ -214,6 +224,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * Bits are read with bitwise AND against each mask; omitting both masks ⇒ pre-fee + returndata. */ function swap( + bytes32 requestHash, InputData calldata input, uint256 flags, FeeData calldata fee, @@ -261,6 +272,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { finalAmount -= feeAmount; } } + + emit RequestExecuted(requestHash); } // ========================================================================= @@ -269,11 +282,13 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre/post swap fee → swap → bridge with runtime amount splicing. + * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. * @param flags Same packing as `swap`; additionally bit 2 forwards final amount as bridge msg.value. * @param fee Set `amount` to 0 to skip fee collection. * @dev Same `minOutput` rule as `swap`: validated on gross `_execSwap` output, then optional output fee applies. */ function swapAndBridge( + bytes32 requestHash, InputData calldata input, uint256 flags, FeeData calldata fee, @@ -292,6 +307,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { uint256 finalAmount = _swapAndBridgeSwap(input, flags, fee, swapData, swapCallData); _finishSwapAndBridge(swapData.outputToken, finalAmount, bridgeData, bridgeCallData, flags); + emit RequestExecuted(requestHash); } function _swapAndBridgeSwap( @@ -354,6 +370,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre-bridge fee → bridge, with no swap step. + * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is * fully knowable by the caller before signing. The caller must therefore * bake the correct amount directly into `bridgeCallData` and set @@ -364,6 +381,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * inputs so that `_msgSender()` resolves to `input.user`. */ function bridge( + bytes32 requestHash, InputData calldata input, FeeData calldata fee, BridgeData calldata bridgeData, @@ -396,6 +414,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // 4. bridge call — data and value are pre-encoded by the caller _doCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); + emit RequestExecuted(requestHash); } // ========================================================================= @@ -405,9 +424,15 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Runs a sequence of generic actions with optional returndata * splicing between steps. No signature verification. + * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. */ - function performModularExecution(Action[] calldata actions) external payable returns (bytes[] memory results) { + function performModularExecution(bytes32 requestHash, Action[] calldata actions) + external + payable + returns (bytes[] memory results) + { results = _performActions(actions); + emit RequestExecuted(requestHash); } // ========================================================================= From 00e3801f3fc84f9fe18fe37cc04081fefe52b948 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Sat, 16 May 2026 00:18:15 +0530 Subject: [PATCH 35/69] refactor: tests --- scripts/e2e/approveViaModular.ts | 2 + .../e2e/arbitrum/performExecution.postFee.ts | 171 +++++++++++ .../performModularExecution.postFee.ts | 165 +++++++++++ scripts/e2e/bridgeViaRelay.ts | 4 +- scripts/e2e/bridgeViaRelaySimple.ts | 9 +- scripts/e2e/cctp/performExecution.postFee.ts | 161 +++++++++++ scripts/e2e/cctp/performExecution.preFee.ts | 115 ++++++++ .../cctp/performModularExecution.postFee.ts | 158 +++++++++++ .../cctp/performModularExecution.preFee.ts | 114 ++++++++ .../cctp/swapAndBridge.postFee.balanceOf.ts | 219 +++++++++++++++ .../cctp/swapAndBridge.postFee.returndata.ts | 219 +++++++++++++++ .../cctp/swapAndBridge.preFee.balanceOf.ts | 221 +++++++++++++++ .../cctp/swapAndBridge.preFee.returndata.ts | 221 +++++++++++++++ scripts/e2e/config.ts | 13 +- scripts/e2e/oft/performExecution.postFee.ts | 183 ++++++++++++ scripts/e2e/oft/performExecution.preFee.ts | 137 +++++++++ .../oft/performModularExecution.postFee.ts | 178 ++++++++++++ .../e2e/oft/performModularExecution.preFee.ts | 134 +++++++++ .../oft/swapAndBridge.postFee.balanceOf.ts | 262 +++++++++++++++++ .../oft/swapAndBridge.postFee.returndata.ts | 262 +++++++++++++++++ .../e2e/oft/swapAndBridge.preFee.balanceOf.ts | 261 +++++++++++++++++ .../oft/swapAndBridge.preFee.returndata.ts | 261 +++++++++++++++++ ...arbUsdcBaseEth.performExecution.postFee.ts | 191 +++++++++++++ ...BaseEth.performModularExecution.postFee.ts | 178 ++++++++++++ ...baseUsdcArbEth.performExecution.postFee.ts | 184 ++++++++++++ ...cArbEth.performModularExecution.postFee.ts | 174 ++++++++++++ ...gonPolUsdt0Arb.performExecution.postFee.ts | 229 +++++++++++++++ ...sdt0Arb.performModularExecution.postFee.ts | 210 ++++++++++++++ ...olygonUsdcBase.performExecution.postFee.ts | 135 +++++++++ ...sdcBase.performModularExecution.postFee.ts | 133 +++++++++ .../swapAndBridge.postFee.balanceOf.ts | 263 +++++++++++++++++ .../swapAndBridge.postFee.returndata.ts | 265 ++++++++++++++++++ .../swapAndBridge.preFee.balanceOf.ts | 261 +++++++++++++++++ .../swapAndBridge.preFee.returndata.ts | 262 +++++++++++++++++ scripts/e2e/swap/swap.postFee.balanceOf.ts | 174 ++++++++++++ scripts/e2e/swap/swap.postFee.returndata.ts | 169 +++++++++++ scripts/e2e/swap/swap.preFee.balanceOf.ts | 174 ++++++++++++ scripts/e2e/swap/swap.preFee.returndata.ts | 169 +++++++++++ scripts/e2e/swapBridgeViaArbitrumNative.ts | 7 +- scripts/e2e/swapBridgeViaCctp.ts | 7 +- scripts/e2e/swapBridgeViaCctpSimple.ts | 3 +- scripts/e2e/swapBridgeViaOft.ts | 7 +- scripts/e2e/swapBridgeViaStargateNative.ts | 7 +- scripts/e2e/utils/contractTypes.ts | 15 +- scripts/e2e/utils/reproducibility.ts | 12 +- scripts/e2e/utils/routerAbi.ts | 27 +- 46 files changed, 6702 insertions(+), 24 deletions(-) create mode 100644 scripts/e2e/arbitrum/performExecution.postFee.ts create mode 100644 scripts/e2e/arbitrum/performModularExecution.postFee.ts create mode 100644 scripts/e2e/cctp/performExecution.postFee.ts create mode 100644 scripts/e2e/cctp/performExecution.preFee.ts create mode 100644 scripts/e2e/cctp/performModularExecution.postFee.ts create mode 100644 scripts/e2e/cctp/performModularExecution.preFee.ts create mode 100644 scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts create mode 100644 scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts create mode 100644 scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts create mode 100644 scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts create mode 100644 scripts/e2e/oft/performExecution.postFee.ts create mode 100644 scripts/e2e/oft/performExecution.preFee.ts create mode 100644 scripts/e2e/oft/performModularExecution.postFee.ts create mode 100644 scripts/e2e/oft/performModularExecution.preFee.ts create mode 100644 scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts create mode 100644 scripts/e2e/oft/swapAndBridge.postFee.returndata.ts create mode 100644 scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts create mode 100644 scripts/e2e/oft/swapAndBridge.preFee.returndata.ts create mode 100644 scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts create mode 100644 scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts create mode 100644 scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts create mode 100644 scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts create mode 100644 scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts create mode 100644 scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts create mode 100644 scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts create mode 100644 scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts create mode 100644 scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts create mode 100644 scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts create mode 100644 scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts create mode 100644 scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts create mode 100644 scripts/e2e/swap/swap.postFee.balanceOf.ts create mode 100644 scripts/e2e/swap/swap.postFee.returndata.ts create mode 100644 scripts/e2e/swap/swap.preFee.balanceOf.ts create mode 100644 scripts/e2e/swap/swap.preFee.returndata.ts diff --git a/scripts/e2e/approveViaModular.ts b/scripts/e2e/approveViaModular.ts index 3d9f34e..b110e7f 100644 --- a/scripts/e2e/approveViaModular.ts +++ b/scripts/e2e/approveViaModular.ts @@ -26,6 +26,7 @@ dotenv.config(); import { CHAIN_IDS, routerAddressForChain, RPC, TOKENS } from './config'; import { encodeApprove } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; +import { ZERO_BYTES32 } from './utils/contractTypes'; // ─── actionInfo helpers ─────────────────────────────────────────────────────── @@ -96,6 +97,7 @@ async function run(): Promise { ]; const calldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, actions, ]); diff --git a/scripts/e2e/arbitrum/performExecution.postFee.ts b/scripts/e2e/arbitrum/performExecution.postFee.ts new file mode 100644 index 0000000..c05764a --- /dev/null +++ b/scripts/e2e/arbitrum/performExecution.postFee.ts @@ -0,0 +1,171 @@ +/** + * Route: Ethereum AAVE → ETH (OpenOcean) → Arbitrum ETH (inbox depositEth) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap + * + * BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to inbox.depositEth(). + * Input is AAVE (ERC-20) so AllowanceHolder.exec is required. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + BRIDGE_VALUE_FLAG, + NO_FEE, + ZERO_ADDRESS, + ZERO_BYTES32, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_ETH, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ETH, + account: ROUTER_ETH, + gasPrice: '20', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); + return fallback; + } +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance, decimals } = await getWalletErc20Balance(TOKENS.AAVE_ETH, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Ethereum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, decimals)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → ETH)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + if (estimatedOut < feeAmount + arbFee) { + console.warn(` Warning: estimated ETH may be insufficient to cover fee + bridge cost`); + } + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); + await ensureRouterNativeBalance(signer, ROUTER_ETH); + await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: 0n }, + flags: BRIDGE_VALUE_FLAG, + }, + swapCallData: swapData, + bridgeCallData: buildDepositEthCalldata(), + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ETH, TOKENS.AAVE_ETH, inputAmount, ROUTER_ETH, callData, 0n); + + logTxnSummary( + 'Ethereum AAVE → Arbitrum ETH (depositEth) — performExecution postFee', + CHAIN_IDS.ETHEREUM, + receipt, + ); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/arbitrum/performModularExecution.postFee.ts b/scripts/e2e/arbitrum/performModularExecution.postFee.ts new file mode 100644 index 0000000..f3c01c6 --- /dev/null +++ b/scripts/e2e/arbitrum/performModularExecution.postFee.ts @@ -0,0 +1,165 @@ +/** + * Route: Ethereum AAVE → ETH (OpenOcean) → Arbitrum ETH (inbox depositEth) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — AAVE → ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) — ETH fee to signer + * [4] nativeCall(inbox, depositEth(), bridgeValue) + * + * Input is AAVE (ERC-20) so AllowanceHolder.exec is required. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performModularExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_ETH, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ETH, + account: ROUTER_ETH, + gasPrice: '20', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); + return fallback; + } +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance, decimals } = await getWalletErc20Balance(TOKENS.AAVE_ETH, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Ethereum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, decimals)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → ETH)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + // bridgeValue uses minAmountOut-based floor so the nativeCall carries at least the bridge cost + const bridgeValue = minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n; + console.log(` Bridge value: ${ethers.formatEther(bridgeValue)} ETH (floor for nativeCall)`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); + await ensureRouterNativeBalance(signer, ROUTER_ETH); + await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_ETH, signerAddress, ROUTER_ETH, inputAmount])); + exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ETH, TOKENS.AAVE_ETH, inputAmount, ROUTER_ETH, callData, 0n); + + logTxnSummary( + 'Ethereum AAVE → Arbitrum ETH (depositEth) — performModularExecution postFee', + CHAIN_IDS.ETHEREUM, + receipt, + ); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); + + // Suppress unused-variable warning for arbFee (kept for informational logging above) + void arbFee; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts index e464516..60b7c08 100644 --- a/scripts/e2e/bridgeViaRelay.ts +++ b/scripts/e2e/bridgeViaRelay.ts @@ -40,7 +40,7 @@ import { import { ROUTER_ABI } from './utils/routerAbi'; import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { MonolithicExecutionCall, NO_FEE, NO_SWAP, monolithicArgs } from './utils/contractTypes'; +import { MonolithicExecutionCall, NO_FEE, NO_SWAP, ZERO_BYTES32, monolithicArgs } from './utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; import { sleep } from './utils/sleep'; import { logTxnSummary } from './utils/txnLogSummary'; @@ -164,6 +164,7 @@ async function executeLeg(args: { depositData, ); execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, actions, ]); } else { @@ -310,6 +311,7 @@ async function executeLegUsdcPolygonToBase(args: { depositData, ); execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, actions, ]); } else { diff --git a/scripts/e2e/bridgeViaRelaySimple.ts b/scripts/e2e/bridgeViaRelaySimple.ts index 3577866..4a7a78f 100644 --- a/scripts/e2e/bridgeViaRelaySimple.ts +++ b/scripts/e2e/bridgeViaRelaySimple.ts @@ -35,6 +35,7 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; +import { ZERO_BYTES32 } from './utils/contractTypes'; import type { BridgeData, FeeData, InputData } from './utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; import { logTxnSummary } from './utils/txnLogSummary'; @@ -73,7 +74,13 @@ function buildBridgeCalldata(routerIface: ethers.Interface, p: BridgeParams): st value: 0n, }; - return routerIface.encodeFunctionData('bridge', [input, p.fee, bridgeData, p.depositData]); + return routerIface.encodeFunctionData('bridge', [ + ZERO_BYTES32, + input, + p.fee, + bridgeData, + p.depositData, + ]); } // ─── Execution leg ──────────────────────────────────────────────────────────── diff --git a/scripts/e2e/cctp/performExecution.postFee.ts b/scripts/e2e/cctp/performExecution.postFee.ts new file mode 100644 index 0000000..45d8544 --- /dev/null +++ b/scripts/e2e/cctp/performExecution.postFee.ts @@ -0,0 +1,161 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of estimatedOut USDC deducted after swap + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + ZERO_BYTES32, + bridgeAmountPositionFlag, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log('Fetching OpenOcean quote (AAVE → USDC)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, + flags: bridgeAmountPositionFlag(4), + }, + swapCallData: swapData, + bridgeCallData: depositForBurnData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon AAVE → Base USDC (CCTP) — performExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/cctp/performExecution.preFee.ts b/scripts/e2e/cctp/performExecution.preFee.ts new file mode 100644 index 0000000..07808f6 --- /dev/null +++ b/scripts/e2e/cctp/performExecution.preFee.ts @@ -0,0 +1,115 @@ +/** + * Route: Polygon USDC → Base USDC (CCTP depositForBurn, no swap) + * Function: performExecution (monolithic) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + ZERO_BYTES32, + bridgeAmountPositionFlag, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(inputAmount - feeAmount, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }, + preFee: { receiver: signerAddress, amount: feeAmount }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, + flags: bridgeAmountPositionFlag(4), + }, + swapCallData: '0x', + bridgeCallData: depositForBurnData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon USDC → Base USDC (CCTP) — performExecution preFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/cctp/performModularExecution.postFee.ts b/scripts/e2e/cctp/performModularExecution.postFee.ts new file mode 100644 index 0000000..719e0b9 --- /dev/null +++ b/scripts/e2e/cctp/performModularExecution.postFee.ts @@ -0,0 +1,158 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of estimatedOut USDC transferred to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] ooRouter swap calldata — AAVE → USDC lands in router + * [3] USDC.transfer(signer, feeAmount) — post-swap fee + * [4] USDC.approve(tokenMessenger, MaxUint256) + * [5] STATICCALL USDC.balanceOf(router) + * [6] tokenMessenger.depositForBurn(...) — spliceArg(0) patches amount from [5] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performModularExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log('Fetching OpenOcean quote (AAVE → USDC)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(polyCctp.tokenMessenger, ethers.MaxUint256)); + const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); + exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon AAVE → Base USDC (CCTP) — performModularExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/cctp/performModularExecution.preFee.ts b/scripts/e2e/cctp/performModularExecution.preFee.ts new file mode 100644 index 0000000..b432ff9 --- /dev/null +++ b/scripts/e2e/cctp/performModularExecution.preFee.ts @@ -0,0 +1,114 @@ +/** + * Route: Polygon USDC → Base USDC (CCTP depositForBurn, no swap) + * Function: performModularExecution (modular) + * Fee: preFee — FEE_BPS of inputAmount USDC transferred to signer before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.transfer(signer, feeAmount) — pre-bridge fee + * [2] USDC.approve(tokenMessenger, MaxUint256) + * [3] STATICCALL USDC.balanceOf(router) + * [4] tokenMessenger.depositForBurn(...) — spliceArg(0) patches amount from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performModularExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(inputAmount - feeAmount, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_POLYGON_CIRCLE, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(polyCctp.tokenMessenger, ethers.MaxUint256)); + const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); + exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon USDC → Base USDC (CCTP) — performModularExecution preFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts new file mode 100644 index 0000000..f622b53 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts @@ -0,0 +1,219 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | balance-of (0x02) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x03n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, balanceOf, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: polyCctp.tokenMessenger, + value: 0n, + }, + depositForBurnData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts new file mode 100644 index 0000000..c12c1ba --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts @@ -0,0 +1,219 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x01n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: polyCctp.tokenMessenger, + value: 0n, + }, + depositForBurnData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts new file mode 100644 index 0000000..78ba078 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts @@ -0,0 +1,221 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | balance-of (0x02) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x02n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, balanceOf, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, minAmountOut } = await fetchOpenOceanQuote( + inputAmount + ); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInputAmount = inputAmount - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInputAmount, 18)} AAVE`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: polyCctp.tokenMessenger, + value: 0n, + }, + depositForBurnData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts new file mode 100644 index 0000000..c72b884 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts @@ -0,0 +1,221 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, returndata, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, minAmountOut } = await fetchOpenOceanQuote( + inputAmount + ); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInputAmount = inputAmount - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInputAmount, 18)} AAVE`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: polyCctp.tokenMessenger, + value: 0n, + }, + depositForBurnData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index c14576d..6e6518c 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -34,7 +34,7 @@ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. */ export const ROUTER_BY_CHAIN_ID: Record = { - [CHAIN_IDS.POLYGON]: '0x5bfbF2d49658e48D209449B3E263DC6F774B6E6f', + [CHAIN_IDS.POLYGON]: '0x7A113007177BF1cd86da69Dbd7d601dcEC9EbAbD', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', [CHAIN_IDS.BASE]: '0x96E8c261fCCDFca2CCffe8b4A33dC8a65f153785', [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', @@ -206,6 +206,17 @@ export const STARGATE_AMOUNT_LD_OFFSET = 196; /** Fee applied in scripts that take pre-/post-route fees (basis points). */ export const FEE_BPS = Number(process.env.FEE_AMOUNT_BPS ?? '10'); +/** + * OpenOcean slippage tolerance used when fetching swap quotes. + * The value is passed directly to OO's `slippage` API parameter (percentage string, e.g. '3' = 3%). + * OO embeds this as `minReturn` in the swap calldata — if the actual on-chain output falls below + * `estimatedOut * (1 - slippage/100)`, OO reverts with "Return amount is not enough". + * AAVE's multi-hop route (AAVE→WMATIC→DAI→USDC) can move 2–3% between quote and execution, + * so 1% is too tight; 3% provides a safe margin while still protecting against severe slippage. + * Override via env: OO_SLIPPAGE_PERCENT=5 + */ +export const OO_SLIPPAGE_PERCENT = process.env.OO_SLIPPAGE_PERCENT ?? '3'; + export function bpsOf(amount: bigint, bps: number): bigint { return (amount * BigInt(bps)) / 10000n; } diff --git a/scripts/e2e/oft/performExecution.postFee.ts b/scripts/e2e/oft/performExecution.postFee.ts new file mode 100644 index 0000000..eb4a2d1 --- /dev/null +++ b/scripts/e2e/oft/performExecution.postFee.ts @@ -0,0 +1,183 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of estimatedOut USDT0 deducted after swap + * + * Bridge amount position flag splices actual post-fee balance into send() amountLD at byte 196. + * bridge.value = nativeFeeWithBuffer (5% buffer on LZ fee) forwarded as LZ msg.value. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + ZERO_BYTES32, + bridgeAmountPositionFlag, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const OFT_AMOUNT_LD_OFFSET = 196; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → USDT0)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, + flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), + }, + swapCallData: swapData, + bridgeCallData: oftSendData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon AAVE → Arbitrum USDT0 (OFT) — performExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/oft/performExecution.preFee.ts b/scripts/e2e/oft/performExecution.preFee.ts new file mode 100644 index 0000000..a26827f --- /dev/null +++ b/scripts/e2e/oft/performExecution.preFee.ts @@ -0,0 +1,137 @@ +/** + * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) + * Function: performExecution (monolithic) + * Fee: preFee — FEE_BPS of inputAmount USDT0 deducted before bridge + * + * Bridge amount position flag splices actual post-fee balance into send() amountLD at byte 196. + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + ZERO_BYTES32, + bridgeAmountPositionFlag, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const OFT_AMOUNT_LD_OFFSET = 196; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDT0_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.USDT0_POLYGON, inputAmount }, + preFee: { receiver: signerAddress, amount: feeAmount }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, + flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), + }, + swapCallData: '0x', + bridgeCallData: oftSendData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDT0_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon USDT0 → Arbitrum USDT0 (OFT direct) — performExecution preFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/oft/performModularExecution.postFee.ts b/scripts/e2e/oft/performModularExecution.postFee.ts new file mode 100644 index 0000000..e59d095 --- /dev/null +++ b/scripts/e2e/oft/performModularExecution.postFee.ts @@ -0,0 +1,178 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of estimatedOut USDT0 transferred to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] ooRouter swap — AAVE → USDT0 lands in router + * [3] USDT0.transfer(signer, feeAmount) + * [4] USDT0.approve(adapter, MaxUint256) + * [5] STATICCALL USDT0.balanceOf(router) + * [6] nativeCall adapter.send(...) — spliceWord patches amountLD at offset 196 from [5] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performModularExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const OFT_AMOUNT_LD_OFFSET = 196; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → USDT0)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon AAVE → Arbitrum USDT0 (OFT) — performModularExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/oft/performModularExecution.preFee.ts b/scripts/e2e/oft/performModularExecution.preFee.ts new file mode 100644 index 0000000..097ef85 --- /dev/null +++ b/scripts/e2e/oft/performModularExecution.preFee.ts @@ -0,0 +1,134 @@ +/** + * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) + * Function: performModularExecution (modular) + * Fee: preFee — FEE_BPS of inputAmount USDT0 transferred to signer before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(USDT0, signer, router, inputAmount) + * [1] USDT0.transfer(signer, feeAmount) — pre-bridge fee + * [2] USDT0.approve(adapter, MaxUint256) + * [3] STATICCALL USDT0.balanceOf(router) + * [4] nativeCall adapter.send(...) — spliceWord patches amountLD at offset 196 from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performModularExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const OFT_AMOUNT_LD_OFFSET = 196; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDT0_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDT0_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDT0_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon USDT0 → Arbitrum USDT0 (OFT direct) — performModularExecution preFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts new file mode 100644 index 0000000..b291e9b --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts @@ -0,0 +1,262 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: post-fee (fee taken from USDT0 output after swap), output measured as USDT0 balanceOf delta + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDT0, deducted from swap output. + * BalanceOf (bit1=1): final USDT0 amount is measured as router USDT0 balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | balance-of (0x02) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x03n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, balanceOf, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDT0 output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatUnits(bridgeAmount, 6)}`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }, + oftSendData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts new file mode 100644 index 0000000..7d76600 --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts @@ -0,0 +1,262 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: post-fee (fee taken from USDT0 output after swap), output read from swap returndata word 0 + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDT0, deducted from swap output. + * Returndata (bit1=0): final USDT0 amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x01n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDT0 output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatUnits(bridgeAmount, 6)}`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }, + oftSendData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts new file mode 100644 index 0000000..f1e91ab --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts @@ -0,0 +1,261 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDT0 balanceOf delta + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * BalanceOf (bit1=1): final USDT0 amount is measured as router USDT0 balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | balance-of (0x02) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x02n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, balanceOf, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = estimatedOut; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }, + oftSendData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts new file mode 100644 index 0000000..5e6353b --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts @@ -0,0 +1,261 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * Returndata (bit1=0): final USDT0 amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, returndata, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = estimatedOut; // estimated bridge amount (no post-fee subtraction here) + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }, + oftSendData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts new file mode 100644 index 0000000..df2b82e --- /dev/null +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -0,0 +1,191 @@ +/** + * Route: Arbitrum USDC → ETH (OpenOcean) → Base ETH (Stargate Native ETH Pool) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap + * + * BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to Stargate. + * amountLD = minAmountOut - fee - nativeFeeWithBuffer (pre-encoded in calldata). + * StargatePoolNative check: msg.value >= amountLD + nativeFee; satisfied since actual >= min. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + BRIDGE_VALUE_FLAG, + NO_FEE, + ZERO_ADDRESS, + ZERO_BYTES32, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFee: bigint; nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_ARB, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFee, + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_ARB, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Arbitrum)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Arbitrum → Base native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // amountLD pre-encoded: minAmountOut - fee - nativeFeeWithBuffer + const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.USDC_ARB, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, value: 0n }, + flags: BRIDGE_VALUE_FLAG, + }, + swapCallData: swapData, + bridgeCallData: stargateData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData); + + logTxnSummary( + 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performExecution postFee', + CHAIN_IDS.ARBITRUM, + receipt, + ); + + console.log('\nETH arrives on Base once LZ delivers the message.'); + + void STARGATE_AMOUNT_LD_OFFSET; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts new file mode 100644 index 0000000..3f301dd --- /dev/null +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts @@ -0,0 +1,178 @@ +/** + * Route: Arbitrum USDC → ETH (OpenOcean) → Base ETH (Stargate Native ETH Pool) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — USDC → ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) — ETH fee out + * [4] nativeCall(Stargate, sendData, bridgeValue) — value = amountLD + nativeFeeWithBuffer + * + * amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer (pre-encoded, surplus stays in router). + * bridgeValue = minAmountOut - feeAmount (amountLD + nativeFeeWithBuffer). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_ARB, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_ARB, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Arbitrum)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Arbitrum → Base native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + // bridgeValue = amountLD + nativeFeeWithBuffer = minAmountOut - feeAmount + const bridgeValue = minAmountOut - feeAmount; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_ARB, signerAddress, ROUTER_ARB, inputAmount])); + exec.call(TOKENS.USDC_ARB, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(STARGATE_NATIVE_ARB, stargateData, bridgeValue); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData); + + logTxnSummary( + 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performModularExecution postFee', + CHAIN_IDS.ARBITRUM, + receipt, + ); + + console.log('\nETH arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts new file mode 100644 index 0000000..ac1647b --- /dev/null +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -0,0 +1,184 @@ +/** + * Route: Base USDC → ETH (OpenOcean) → Arbitrum ETH (Stargate Native ETH Pool) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap + * + * BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to Stargate. + * amountLD = minAmountOut - fee - nativeFeeWithBuffer (pre-encoded in calldata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + BRIDGE_VALUE_FLAG, + NO_FEE, + ZERO_ADDRESS, + ZERO_BYTES32, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_BASE, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_BASE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Base)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Base → Arbitrum native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.USDC_BASE, inputAmount }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: 0n }, + flags: BRIDGE_VALUE_FLAG, + }, + swapCallData: swapData, + bridgeCallData: stargateData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData); + + logTxnSummary( + 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performExecution postFee', + CHAIN_IDS.BASE, + receipt, + ); + + console.log('\nETH arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts new file mode 100644 index 0000000..7eb6f04 --- /dev/null +++ b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts @@ -0,0 +1,174 @@ +/** + * Route: Base USDC → ETH (OpenOcean) → Arbitrum ETH (Stargate Native ETH Pool) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — USDC → ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) + * [4] nativeCall(Stargate, sendData, bridgeValue) — value = amountLD + nativeFeeWithBuffer + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_BASE, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_BASE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Base)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Base → Arbitrum native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + const bridgeValue = minAmountOut - feeAmount; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_BASE, signerAddress, ROUTER_BASE, inputAmount])); + exec.call(TOKENS.USDC_BASE, encodeApprove(ooRouter, inputAmount)); + exec.call(ooRouter, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(STARGATE_NATIVE_BASE, stargateData, bridgeValue); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData); + + logTxnSummary( + 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performModularExecution postFee', + CHAIN_IDS.BASE, + receipt, + ); + + console.log('\nETH arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts new file mode 100644 index 0000000..b56d40e --- /dev/null +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts @@ -0,0 +1,229 @@ +/** + * Route: Polygon POL (native) → USDT0 (OpenOcean) → Arbitrum USDT0 (LZ OFT Adapter) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of estimatedOut USDT0 deducted after swap + * + * Input is native POL; msg.value = ooSwapNativeWei + nativeFeeWithBuffer. + * swap.value = POL forwarded to OO router; bridge.value = nativeFeeWithBuffer (LZ fee). + * Bridge amount position flag splices actual post-fee USDT0 balance at byte 196. + * + * For native-input cases this script must be run with sufficient POL balance. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers, parseEther } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + USDT0_OFT_ADAPTER_POLYGON, + ARBITRUM_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH } from '../utils/allowanceHolder'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + ZERO_ADDRESS, + ZERO_BYTES32, + bridgeAmountPositionFlag, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); +const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; value?: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmountWei: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + nativeSwapWei: bigint; +}> { + const params: Record = { + inTokenAddress: NATIVE_TOKEN_ADDRESS, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatEther(inputAmountWei), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + nativeSwapWei: q.value !== undefined && q.value !== '' ? BigInt(q.value) : 0n, + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + if (rawBalance <= NATIVE_INPUT_GAS_RESERVE) { + throw new Error(`Signer ${signerAddress} POL balance (${ethers.formatEther(rawBalance)}) below reserve`); + } + + const feeData = await provider.getFeeData(); + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice ?? 500_000_000n; + const gasReserve = maxFeePerGas * NATIVE_INPUT_GAS_LIMIT_ESTIMATE; + console.log(` Gas reserve: ${ethers.formatEther(gasReserve)} POL`); + + // Start with full usable balance; capped below if lz fee eats too much + let inputAmountWei = rawBalance - NATIVE_INPUT_GAS_RESERVE - 20n; + if (inputAmountWei <= 0n) throw new Error('POL balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`POL balance: ${ethers.formatEther(rawBalance)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + let ooRouter = ''; + let swapData = ''; + let nativeSwapWei = 0n; + let feeAmount = 0n; + let estimatedBridgeAmount = 0n; + let minAmountOut = 0n; + let nativeFeeWithBuffer = 0n; + let amountReceivedLD = 0n; + + // Re-quote loop: cap inputAmountWei if balance can't cover lz fee + gas reserve + for (let iter = 0; iter < 6; iter++) { + console.log('Fetching OpenOcean quote (POL → USDT0)...'); + const q = await fetchOpenOceanQuote(inputAmountWei); + ooRouter = q.ooRouter; + swapData = q.swapData; + nativeSwapWei = q.nativeSwapWei; + feeAmount = bpsOf(q.estimatedOut, FEE_BPS); + estimatedBridgeAmount = q.estimatedOut - feeAmount; + minAmountOut = q.minAmountOut; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(q.estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + ({ nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, estimatedBridgeAmount, signerAddress)); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const maxAffordable = rawBalance - nativeFeeWithBuffer - gasReserve; + if (maxAffordable <= 0n) { + throw new Error(`POL balance cannot cover lz fee (${ethers.formatEther(nativeFeeWithBuffer)}) + gas reserve`); + } + if (inputAmountWei <= maxAffordable) { + break; + } + console.warn(` Capping swap input from ${ethers.formatEther(inputAmountWei)} to ${ethers.formatEther(maxAffordable)} POL`); + inputAmountWei = maxAffordable; + } + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterNativeBalance(signer, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; + const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount: inputAmountWei }, + preFee: NO_FEE, + swap: { + target: ooRouter, + approvalSpender: ZERO_ADDRESS, + outputToken: TOKENS.USDT0_POLYGON, + value: polOrEthToOo, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, + flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), + }, + swapCallData: swapData, + bridgeCallData: oftSendData, + }; + + const txValue = inputAmountWei + nativeFeeWithBuffer; + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + // Native input — no ERC-20 allowance needed for AH; pass NATIVE_TOKEN_ADDRESS + const receipt = await execViaAH(signer, ROUTER_POLYGON, NATIVE_TOKEN_ADDRESS, inputAmountWei, ROUTER_POLYGON, callData, txValue); + + logTxnSummary( + 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (OFT) — performExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); + + void amountReceivedLD; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts new file mode 100644 index 0000000..2bd1c19 --- /dev/null +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts @@ -0,0 +1,210 @@ +/** + * Route: Polygon POL (native) → USDT0 (OpenOcean) → Arbitrum USDT0 (LZ OFT Adapter) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of estimatedOut USDT0 transferred to signer after swap + * + * Input is native POL; msg.value = ooSwapNativeWei + nativeFeeWithBuffer. + * + * Modular action sequence: + * [0] nativeCall(ooRouter, swapData, polOrEthToOo) — POL → USDT0 lands in router + * [1] USDT0.transfer(signer, feeAmount) + * [2] USDT0.approve(adapter, MaxUint256) + * [3] STATICCALL USDT0.balanceOf(router) + * [4] nativeCall(adapter, oftSendData, nativeFeeWithBuffer) — splicePayloadWord(196) from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers, parseEther } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + USDT0_OFT_ADAPTER_POLYGON, + ARBITRUM_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); +const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; value?: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmountWei: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + nativeSwapWei: bigint; +}> { + const params: Record = { + inTokenAddress: NATIVE_TOKEN_ADDRESS, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatEther(inputAmountWei), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + nativeSwapWei: q.value !== undefined && q.value !== '' ? BigInt(q.value) : 0n, + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + if (rawBalance <= NATIVE_INPUT_GAS_RESERVE) { + throw new Error(`Signer ${signerAddress} POL balance (${ethers.formatEther(rawBalance)}) below reserve`); + } + + const feeData = await provider.getFeeData(); + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice ?? 500_000_000n; + const gasReserve = maxFeePerGas * NATIVE_INPUT_GAS_LIMIT_ESTIMATE; + console.log(` Gas reserve: ${ethers.formatEther(gasReserve)} POL`); + + let inputAmountWei = rawBalance - NATIVE_INPUT_GAS_RESERVE - 20n; + if (inputAmountWei <= 0n) throw new Error('POL balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`POL balance: ${ethers.formatEther(rawBalance)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + let ooRouter = ''; + let swapData = ''; + let nativeSwapWei = 0n; + let feeAmount = 0n; + let estimatedBridgeAmount = 0n; + let nativeFeeWithBuffer = 0n; + let amountReceivedLD = 0n; + + for (let iter = 0; iter < 6; iter++) { + console.log('Fetching OpenOcean quote (POL → USDT0)...'); + const q = await fetchOpenOceanQuote(inputAmountWei); + ooRouter = q.ooRouter; + swapData = q.swapData; + nativeSwapWei = q.nativeSwapWei; + feeAmount = bpsOf(q.estimatedOut, FEE_BPS); + estimatedBridgeAmount = q.estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(q.estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + ({ nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, estimatedBridgeAmount, signerAddress)); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const maxAffordable = rawBalance - nativeFeeWithBuffer - gasReserve; + if (maxAffordable <= 0n) { + throw new Error(`POL balance cannot cover lz fee (${ethers.formatEther(nativeFeeWithBuffer)}) + gas reserve`); + } + if (inputAmountWei <= maxAffordable) { + break; + } + console.warn(` Capping swap input from ${ethers.formatEther(inputAmountWei)} to ${ethers.formatEther(maxAffordable)} POL`); + inputAmountWei = maxAffordable; + } + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterNativeBalance(signer, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; + const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const exec = new ModularActionsBuilder(); + exec.nativeCall(ooRouter, swapData, polOrEthToOo); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + const txValue = inputAmountWei + nativeFeeWithBuffer; + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + const receipt = await execViaAH(signer, ROUTER_POLYGON, NATIVE_TOKEN_ADDRESS, inputAmountWei, ROUTER_POLYGON, callData, txValue); + + logTxnSummary( + 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (OFT) — performModularExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); + + void amountReceivedLD; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts new file mode 100644 index 0000000..f1de77b --- /dev/null +++ b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -0,0 +1,135 @@ +/** + * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) + * Function: performExecution (monolithic) + * Fee: postFee — FEE_BPS of inputAmount USDC deducted; bridge amount position flag splices + * actual post-fee balance into amountLD at byte 196 of Stargate send() calldata. + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + STARGATE_USDC_POLYGON, + BASE_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + ZERO_BYTES32, + bridgeAmountPositionFlag, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, recipient: string): string { + // amountLD = 0 placeholder; router splices actual balance at STARGATE_AMOUNT_LD_OFFSET + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const estimatedBridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Est. bridge: ${ethers.formatUnits(estimatedBridgeAmount, 6)} USDC`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, STARGATE_USDC_POLYGON); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }, + preFee: NO_FEE, + swap: NO_SWAP, + postFee: { receiver: signerAddress, amount: feeAmount }, + bridge: { target: STARGATE_USDC_POLYGON, approvalSpender: STARGATE_USDC_POLYGON, value: nativeFeeWithBuffer }, + flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), + }, + swapCallData: '0x', + bridgeCallData: stargateData, + }; + + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon USDC → Base USDC (Stargate USDC pool) — performExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDC arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts new file mode 100644 index 0000000..87dd89b --- /dev/null +++ b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts @@ -0,0 +1,133 @@ +/** + * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) + * Function: performModularExecution (modular) + * Fee: postFee — FEE_BPS of inputAmount USDC transferred to signer; staticCall balance spliced + * into Stargate amountLD at STARGATE_AMOUNT_LD_OFFSET (byte 196). + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.transfer(signer, feeAmount) + * [2] USDC.approve(stargatePool, MaxUint256) + * [3] STATICCALL USDC.balanceOf(router) + * [4] nativeCall(Stargate, sendData, nativeFeeWithBuffer) — splicePayloadWord(196) from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, + STARGATE_USDC_POLYGON, + BASE_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const estimatedBridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Est. bridge: ${ethers.formatUnits(estimatedBridgeAmount, 6)} USDC`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, STARGATE_USDC_POLYGON); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_POLYGON_CIRCLE, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(STARGATE_USDC_POLYGON, ethers.MaxUint256)); + const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(STARGATE_USDC_POLYGON, stargateData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); + + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon USDC → Base USDC (Stargate USDC pool) — performModularExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDC arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts new file mode 100644 index 0000000..f65ba2f --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -0,0 +1,263 @@ +/** + * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Flags: post-fee (fee taken from ETH output after swap), output measured as ETH balanceOf delta + * bridge-value flag: router forwards finalETH as msg.value to Stargate + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. + * BalanceOf (bit1=1): final ETH amount is measured as router ETH balance change (not returndata). + * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). + * + * amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, +} from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | balance-of (0x02) | bridge-value (0x04): forward finalETH as msg.value +const FLAGS = 0x03n | BRIDGE_VALUE_FLAG; +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_ARB, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_ARB, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log( + `Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated ETH output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeEstimate = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log( + ` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatEther(bridgeEstimate)}`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote..."); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + bridgeEstimate, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor) + const amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) + throw new Error( + "estimatedOut too small to cover fee + nativeFeeWithBuffer" + ); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.USDC_ARB, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_ARB, + approvalSpender: ZERO_ADDRESS, + value: 0n, + }, + stargateData, + ]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_ARB, + TOKENS.USDC_ARB, + inputAmount, + ROUTER_ARB, + callData, + 0n + ); + + logTxnSummary( + `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.ARBITRUM, + receipt + ); + + console.log("\nETH arrives on Base once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts new file mode 100644 index 0000000..fc7d448 --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -0,0 +1,265 @@ +/** + * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Flags: post-fee (fee taken from ETH output after swap), output read from swap returndata word 0 + * bridge-value flag: router forwards finalETH as msg.value to Stargate + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. + * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. + * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). + * + * amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor). + * StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= minAmountOut. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, +} from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | bridge-value (0x04): forward finalETH as msg.value +const FLAGS = 0x01n | BRIDGE_VALUE_FLAG; +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_ARB, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_ARB, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated ETH output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log( + ` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)` + ); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote..."); + const bridgeEstimate = estimatedOut - feeAmount; + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + bridgeEstimate, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor) + const amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) + throw new Error( + "estimatedOut too small to cover fee + nativeFeeWithBuffer" + ); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.USDC_ARB, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_ARB, + approvalSpender: ZERO_ADDRESS, + value: 0n, + }, + stargateData, + ]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_ARB, + TOKENS.USDC_ARB, + inputAmount, + ROUTER_ARB, + callData, + 0n + ); + + logTxnSummary( + `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge postFee/returndata`, + CHAIN_IDS.ARBITRUM, + receipt + ); + + console.log("\nETH arrives on Base once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts new file mode 100644 index 0000000..069fd53 --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -0,0 +1,261 @@ +/** + * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Flags: pre-fee (fee taken from USDC input before swap), output measured as ETH balanceOf delta + * bridge-value flag: router forwards finalETH as msg.value to Stargate + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount USDC, deducted before the swap. + * BalanceOf (bit1=1): final ETH amount is measured as router ETH balance change (not returndata). + * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). + * + * amountLD = estimatedOut - nativeFeeWithBuffer (conservative floor; actual >= amountLD). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, +} from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | balance-of (0x02) | bridge-value (0x04): forward finalETH as msg.value +const FLAGS = 0x02n | BRIDGE_VALUE_FLAG; +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_ARB, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_ARB, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log( + `Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + // pre-fee: deduct from input USDC before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInput = inputAmount - feeAmount; + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(swapInput); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInput, 6)} USDC`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote..."); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + estimatedOut, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // amountLD = estimatedOut - nativeFeeWithBuffer (conservative floor) + const amountLD = estimatedOut - nativeFeeWithBuffer; + if (amountLD <= 0n) + throw new Error("estimatedOut too small to cover nativeFeeWithBuffer"); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.USDC_ARB, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_ARB, + approvalSpender: ZERO_ADDRESS, + value: 0n, + }, + stargateData, + ]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_ARB, + TOKENS.USDC_ARB, + inputAmount, + ROUTER_ARB, + callData, + 0n + ); + + logTxnSummary( + `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.ARBITRUM, + receipt + ); + + console.log("\nETH arrives on Base once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts new file mode 100644 index 0000000..c897ee8 --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -0,0 +1,262 @@ +/** + * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Flags: pre-fee (fee taken from USDC input before swap), output read from swap returndata word 0 + * bridge-value flag: router forwards finalETH as msg.value to Stargate + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount USDC, deducted before the swap. + * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. + * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). + * + * amountLD is pre-encoded conservatively as estimatedOut - nativeFeeWithBuffer. + * StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= amountLD + buffer. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, +} from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | bridge-value (0x04): forward finalETH as msg.value +const FLAGS = BRIDGE_VALUE_FLAG; +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_ARB, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_ARB, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log( + `Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + // pre-fee: deduct from input USDC before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInput = inputAmount - feeAmount; + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(swapInput); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInput, 6)} USDC`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote..."); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + estimatedOut, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // amountLD = estimatedOut - nativeFeeWithBuffer (conservative floor for pre-encoded calldata) + const amountLD = estimatedOut - nativeFeeWithBuffer; + if (amountLD <= 0n) + throw new Error("estimatedOut too small to cover nativeFeeWithBuffer"); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.USDC_ARB, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_ARB, + approvalSpender: ZERO_ADDRESS, + value: 0n, + }, + stargateData, + ]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_ARB, + TOKENS.USDC_ARB, + inputAmount, + ROUTER_ARB, + callData, + 0n + ); + + logTxnSummary( + `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge preFee/returndata`, + CHAIN_IDS.ARBITRUM, + receipt + ); + + console.log("\nETH arrives on Base once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.postFee.balanceOf.ts b/scripts/e2e/swap/swap.postFee.balanceOf.ts new file mode 100644 index 0000000..50e518c --- /dev/null +++ b/scripts/e2e/swap/swap.postFee.balanceOf.ts @@ -0,0 +1,174 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | balance-of (0x02) +const FLAGS = 0x03n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.postFee.returndata.ts b/scripts/e2e/swap/swap.postFee.returndata.ts new file mode 100644 index 0000000..7280e6f --- /dev/null +++ b/scripts/e2e/swap/swap.postFee.returndata.ts @@ -0,0 +1,169 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | returndata (0x00) +const FLAGS = 0x01n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.preFee.balanceOf.ts b/scripts/e2e/swap/swap.preFee.balanceOf.ts new file mode 100644 index 0000000..7bac539 --- /dev/null +++ b/scripts/e2e/swap/swap.preFee.balanceOf.ts @@ -0,0 +1,174 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | balance-of (0x02) +const FLAGS = 0x02n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.preFee.returndata.ts b/scripts/e2e/swap/swap.preFee.returndata.ts new file mode 100644 index 0000000..61c7d76 --- /dev/null +++ b/scripts/e2e/swap/swap.preFee.returndata.ts @@ -0,0 +1,169 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | returndata (0x00) +const FLAGS = 0x00n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index 71c531b..0b0236c 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -50,6 +50,7 @@ import { bpsOf, RPC, OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, ALLOWANCE_HOLDER, NATIVE_TOKEN_ADDRESS, } from './config'; @@ -67,6 +68,7 @@ import { MonolithicExecutionCall, NO_FEE, ZERO_ADDRESS, + ZERO_BYTES32, monolithicArgs, } from './utils/contractTypes'; import { sleep } from './utils/sleep'; @@ -167,7 +169,6 @@ interface OoSwapQuoteResponse { async function fetchOoQuote( routerAddress: string, inputAmount: bigint, - slippageBps: number = 100, ): Promise<{ ooRouter: string; swapData: string; @@ -178,7 +179,7 @@ async function fetchOoQuote( inTokenAddress: TOKENS.AAVE_ETH, outTokenAddress: NATIVE_TOKEN_ADDRESS, amount: ethers.formatUnits(inputAmount, 18), - slippage: (slippageBps / 100).toString(), + slippage: OO_SLIPPAGE_PERCENT, sender: routerAddress, account: routerAddress, gasPrice: '20', @@ -343,7 +344,7 @@ async function executeLeg( ooRouter, swapData, ); - execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, actions]); } else { const mono = buildMonolithic(signerAddress, inputAmount, feeAmount, minAmountOut, ooRouter, swapData); execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono)); diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts index 22a734a..c55af37 100644 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ b/scripts/e2e/swapBridgeViaCctp.ts @@ -27,6 +27,7 @@ import { bpsOf, RPC, OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, ALLOWANCE_HOLDER, } from './config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; @@ -38,6 +39,7 @@ import { MonolithicExecutionCall, NO_FEE, NO_SWAP, + ZERO_BYTES32, bridgeAmountPositionFlag, monolithicArgs, } from './utils/contractTypes'; @@ -64,7 +66,6 @@ interface OpenOceanSwapQuoteResponse { async function fetchOpenOceanSwapQuote( routerAddress: string, inputAmount: bigint, - slippageBps: number = 100, ): Promise<{ routerAddress: string; swapData: string; @@ -75,7 +76,7 @@ async function fetchOpenOceanSwapQuote( inTokenAddress: TOKENS.AAVE_POLYGON, outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, amount: ethers.formatUnits(inputAmount, 18), - slippage: (slippageBps / 100).toString(), + slippage: OO_SLIPPAGE_PERCENT, sender: routerAddress, account: routerAddress, gasPrice: '1', @@ -286,6 +287,7 @@ async function executeLegUsdcPolygonToBaseCctp(args: { let execCalldata: string; if (useModular) { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, buildModularActionsUsdcPolygonToBaseCctp( signerAddress, ROUTER_POLYGON, @@ -373,6 +375,7 @@ async function executeLeg(args: { let execCalldata: string; if (useModular) { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, buildModularActions( signerAddress, ROUTER_POLYGON, diff --git a/scripts/e2e/swapBridgeViaCctpSimple.ts b/scripts/e2e/swapBridgeViaCctpSimple.ts index d5831e5..5ffc4fb 100644 --- a/scripts/e2e/swapBridgeViaCctpSimple.ts +++ b/scripts/e2e/swapBridgeViaCctpSimple.ts @@ -33,7 +33,7 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; import { getWalletErc20Balance } from './utils/erc20'; import { ROUTER_ABI } from './utils/routerAbi'; -import type { BridgeData, FeeData, InputData } from './utils/contractTypes'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from './utils/contractTypes'; import { logTxnSummary } from './utils/txnLogSummary'; import { ensureRouterErc20Balance, @@ -95,6 +95,7 @@ function buildBridgeCalldata( }; return routerIface.encodeFunctionData('bridge', [ + ZERO_BYTES32, input, args.fee, bridgeData, diff --git a/scripts/e2e/swapBridgeViaOft.ts b/scripts/e2e/swapBridgeViaOft.ts index 5259910..53971f3 100644 --- a/scripts/e2e/swapBridgeViaOft.ts +++ b/scripts/e2e/swapBridgeViaOft.ts @@ -51,6 +51,7 @@ import { bpsOf, RPC, OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, ALLOWANCE_HOLDER, ARBITRUM_LZ_EID, USDT0_OFT_ADAPTER_POLYGON, @@ -72,6 +73,7 @@ import { MonolithicExecutionCall, NO_FEE, NO_SWAP, + ZERO_BYTES32, bridgeAmountPositionFlag, monolithicArgs, } from './utils/contractTypes'; @@ -124,7 +126,6 @@ interface OoSwapQuoteResponse { */ async function fetchOpenOceanQuote( inputAmount: bigint, - slippageBps: number = 100, ): Promise<{ ooRouter: string; swapData: string; @@ -135,7 +136,7 @@ async function fetchOpenOceanQuote( inTokenAddress: TOKENS.AAVE_POLYGON, outTokenAddress: TOKENS.USDT0_POLYGON, amount: ethers.formatUnits(inputAmount, 18), // AAVE has 18 decimals - slippage: (slippageBps / 100).toString(), + slippage: OO_SLIPPAGE_PERCENT, sender: ROUTER_POLYGON, account: ROUTER_POLYGON, gasPrice: '1', @@ -393,6 +394,7 @@ async function executeCase1Leg(args: { let execCalldata: string; if (useModular) { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, buildCase1Modular( signerAddress, inputAmount, @@ -584,6 +586,7 @@ async function executeCase2Leg(args: { let execCalldata: string; if (useModular) { execCalldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, buildCase2Modular( signerAddress, inputAmount, diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index 18cedfe..54961c5 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -68,6 +68,7 @@ import { bpsOf, RPC, OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, ALLOWANCE_HOLDER, NATIVE_TOKEN_ADDRESS, STARGATE_NATIVE_ARB, @@ -94,6 +95,7 @@ import { NO_FEE, NO_SWAP, ZERO_ADDRESS, + ZERO_BYTES32, bridgeAmountPositionFlag, monolithicArgs, } from './utils/contractTypes'; @@ -366,7 +368,6 @@ async function fetchOoQuote( cfg: OoSwapConfig, routerAddress: string, amount: bigint, - slippageBps: number = 100, ): Promise<{ ooRouter: string; swapData: string; @@ -379,7 +380,7 @@ async function fetchOoQuote( inTokenAddress: cfg.inToken, outTokenAddress: cfg.outToken, amount: ethers.formatUnits(amount, cfg.inDecimals), - slippage: (slippageBps / 100).toString(), + slippage: OO_SLIPPAGE_PERCENT, sender: routerAddress, account: routerAddress, gasPrice: cfg.gasPrice, @@ -973,7 +974,7 @@ async function executeLeg( signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, stargateData, ); } - execCalldata = routerIface.encodeFunctionData('performModularExecution', [actions]); + execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, actions]); } else { let mono: MonolithicExecutionCall; if (cfg.isNativePool) { diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index 2721773..9f5dfac 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -52,6 +52,10 @@ export const BRIDGE_AMOUNT_POSITION_FLAG = 8n; export const BRIDGE_AMOUNT_POSITION_SHIFT = 16n; export const MAX_BRIDGE_AMOUNT_POSITION = 0xffffn; +/** 32-byte zero; use as `requestHash` when scripts do not assign a request id. */ +export const ZERO_BYTES32 = + '0x0000000000000000000000000000000000000000000000000000000000000000' as const; + export function bridgeAmountPositionFlag(position: bigint | number): bigint { const positionBigInt = BigInt(position); if (positionBigInt < 0n || positionBigInt > MAX_BRIDGE_AMOUNT_POSITION) { @@ -60,12 +64,11 @@ export function bridgeAmountPositionFlag(position: bigint | number): bigint { return BRIDGE_AMOUNT_POSITION_FLAG | (positionBigInt << BRIDGE_AMOUNT_POSITION_SHIFT); } -export function monolithicArgs(call: MonolithicExecutionCall) { - return [ - call.exec, - call.swapCallData, - call.bridgeCallData, - ] as const; +export function monolithicArgs( + call: MonolithicExecutionCall, + requestHash: string = ZERO_BYTES32, +): readonly [string, MonolithicExecution, string, string] { + return [requestHash, call.exec, call.swapCallData, call.bridgeCallData] as const; } // ─── Sentinel / zero helpers ────────────────────────────────────────────────── diff --git a/scripts/e2e/utils/reproducibility.ts b/scripts/e2e/utils/reproducibility.ts index 6252190..9db88d0 100644 --- a/scripts/e2e/utils/reproducibility.ts +++ b/scripts/e2e/utils/reproducibility.ts @@ -14,7 +14,9 @@ */ import { ethers } from 'ethers'; import { getErc20Contract, encodeApprove } from './erc20'; +import { execViaAH } from './allowanceHolder'; import { ROUTER_ABI } from './routerAbi'; +import { ZERO_BYTES32 } from './contractTypes'; const SEED_WEI = 20n; @@ -100,7 +102,11 @@ export async function ensureRouterApproval( splices: [], }, ]; - const calldata = routerIface.encodeFunctionData('performModularExecution', [actions]); - const tx = await signer.sendTransaction({ to: openRouter, data: calldata }); - await tx.wait(); + const calldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, + actions, + ]); + // Route through AllowanceHolder so _msgSender() resolves correctly inside the router. + // amount=0 because we are not pulling user tokens — we only need AH to forward the call. + await execViaAH(signer, openRouter, tokenResolved, 0n, openRouter, calldata); } diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index 9494de6..206584a 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -3,8 +3,9 @@ * called from e2e scripts. Structs must exactly match the Solidity definitions. */ export const ROUTER_ABI = [ - // Monolithic path + // Monolithic path — `requestHash` is first for indexer-friendly calldata layout `function performExecution( + bytes32 requestHash, ( (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) preFee, @@ -19,11 +20,35 @@ export const ROUTER_ABI = [ // Modular path `function performModularExecution( + bytes32 requestHash, (uint256 actionInfo, bytes data, uint256[] splices)[] actions ) external payable`, + // Standalone swap — pull, optional fee, swap; returns finalAmount + `function swap( + bytes32 requestHash, + (address user, address inputToken, uint256 inputAmount) input, + uint256 flags, + (address receiver, uint256 amount) fee, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, + bytes swapCallData + ) external payable returns (uint256)`, + + // Swap + bridge — pull, optional fee, swap, then bridge with optional amount splicing + `function swapAndBridge( + bytes32 requestHash, + (address user, address inputToken, uint256 inputAmount) input, + uint256 flags, + (address receiver, uint256 amount) fee, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, + bytes swapCallData, + (address target, address approvalSpender, uint256 value) bridgeData, + bytes bridgeCallData + ) external payable`, + // Simple bridge path (no swap, no splicing — caller pre-encodes finalAmount into data) `function bridge( + bytes32 requestHash, (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) fee, (address target, address approvalSpender, uint256 value) bridgeData, From 08d78aac39f9316f4026b466e42a210ae221e045 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 12:43:48 +0530 Subject: [PATCH 36/69] refactor: tests --- .../e2e/arbitrum/performExecution.preFee.ts | 132 +++++++++++++++ .../performModularExecution.preFee.ts | 118 +++++++++++++ scripts/e2e/cctp/bridge.preFee.ts | 118 +++++++++++++ scripts/e2e/oft/bridge.preFee.ts | 160 ++++++++++++++++++ scripts/e2e/relay/aave.bridge.preFee.ts | 97 +++++++++++ .../e2e/relay/aave.performExecution.preFee.ts | 112 ++++++++++++ .../aave.performModularExecution.preFee.ts | 107 ++++++++++++ scripts/e2e/relay/usdc.bridge.preFee.ts | 97 +++++++++++ .../e2e/relay/usdc.performExecution.preFee.ts | 112 ++++++++++++ .../usdc.performModularExecution.preFee.ts | 107 ++++++++++++ .../stargate/polygonUsdcBase.bridge.preFee.ts | 158 +++++++++++++++++ 11 files changed, 1318 insertions(+) create mode 100644 scripts/e2e/arbitrum/performExecution.preFee.ts create mode 100644 scripts/e2e/arbitrum/performModularExecution.preFee.ts create mode 100644 scripts/e2e/cctp/bridge.preFee.ts create mode 100644 scripts/e2e/oft/bridge.preFee.ts create mode 100644 scripts/e2e/relay/aave.bridge.preFee.ts create mode 100644 scripts/e2e/relay/aave.performExecution.preFee.ts create mode 100644 scripts/e2e/relay/aave.performModularExecution.preFee.ts create mode 100644 scripts/e2e/relay/usdc.bridge.preFee.ts create mode 100644 scripts/e2e/relay/usdc.performExecution.preFee.ts create mode 100644 scripts/e2e/relay/usdc.performModularExecution.preFee.ts create mode 100644 scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts diff --git a/scripts/e2e/arbitrum/performExecution.preFee.ts b/scripts/e2e/arbitrum/performExecution.preFee.ts new file mode 100644 index 0000000..63ff1f4 --- /dev/null +++ b/scripts/e2e/arbitrum/performExecution.preFee.ts @@ -0,0 +1,132 @@ +/** + * Route: Ethereum ETH → Arbitrum ETH (Arbitrum inbox depositEth, no swap) + * Function: performExecution (monolithic) + * Fee: preFee — FEE_BPS of inputAmount ETH deducted before bridge + * + * BRIDGE_VALUE_FLAG set: router forwards the remaining ETH after preFee as + * msg.value to inbox.depositEth(). Input is native ETH so we call execDirect + * (no AllowanceHolder needed — router checks msg.value >= inputAmount directly). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + NATIVE_TOKEN_ADDRESS, +} from '../config'; +import { execDirect } from '../utils/allowanceHolder'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + BRIDGE_VALUE_FLAG, + NO_FEE, + NO_SWAP, + ZERO_ADDRESS, + ZERO_BYTES32, + monolithicArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterNativeBalance } from '../utils/reproducibility'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +/** Gas reserve kept in the signer's wallet to cover the transaction itself. */ +const GAS_RESERVE = ethers.parseEther('0.005'); + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); + return fallback; + } +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + const inputAmount = rawBalance - GAS_RESERVE - 20n; + if (inputAmount <= 0n) { + throw new Error(`Signer ${signerAddress} has insufficient ETH on Ethereum (balance: ${ethers.formatEther(rawBalance)})`); + } + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeValue = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`ETH balance: ${ethers.formatEther(rawBalance)}`); + console.log(`Input amount: ${ethers.formatEther(inputAmount)} (balance minus gas reserve)`); + console.log(`Pre-bridge fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(`Bridge value: ${ethers.formatEther(bridgeValue)} ETH`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + if (bridgeValue < arbFee) { + console.warn(` Warning: bridgeValue (${ethers.formatEther(bridgeValue)}) may be below Arbitrum bridge cost (${ethers.formatEther(arbFee)})`); + } + + await ensureRouterNativeBalance(signer, ROUTER_ETH); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, + preFee: { receiver: signerAddress, amount: feeAmount }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: 0n }, + flags: BRIDGE_VALUE_FLAG, + }, + swapCallData: '0x', + bridgeCallData: buildDepositEthCalldata(), + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + // Native ETH input — send directly to the router; no AllowanceHolder needed. + console.log('Sending direct router tx → router.performExecution...'); + const receipt = await execDirect(signer, ROUTER_ETH, callData, inputAmount); + + logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — performExecution preFee', CHAIN_IDS.ETHEREUM, receipt); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); + + void arbFee; +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/arbitrum/performModularExecution.preFee.ts b/scripts/e2e/arbitrum/performModularExecution.preFee.ts new file mode 100644 index 0000000..4048057 --- /dev/null +++ b/scripts/e2e/arbitrum/performModularExecution.preFee.ts @@ -0,0 +1,118 @@ +/** + * Route: Ethereum ETH → Arbitrum ETH (Arbitrum inbox depositEth, no swap) + * Function: performModularExecution (modular) + * Fee: preFee — FEE_BPS of inputAmount ETH deducted before bridge + * + * Modular action sequence: + * [0] nativeCall(signer, '0x', feeAmount) — preFee ETH to signer + * [1] nativeCall(inbox, depositEth(), bridgeValue) — bridge remaining ETH + * + * Input is native ETH so we call execDirect (no AllowanceHolder needed — + * performModularExecution has no _pullFromUser; ETH arrives via msg.value). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performModularExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execDirect } from '../utils/allowanceHolder'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterNativeBalance } from '../utils/reproducibility'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +/** Gas reserve kept in the signer's wallet to cover the transaction itself. */ +const GAS_RESERVE = ethers.parseEther('0.005'); + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); + return fallback; + } +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + const inputAmount = rawBalance - GAS_RESERVE - 20n; + if (inputAmount <= 0n) { + throw new Error(`Signer ${signerAddress} has insufficient ETH on Ethereum (balance: ${ethers.formatEther(rawBalance)})`); + } + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeValue = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`ETH balance: ${ethers.formatEther(rawBalance)}`); + console.log(`Input amount: ${ethers.formatEther(inputAmount)} (balance minus gas reserve)`); + console.log(`Pre-bridge fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(`Bridge value: ${ethers.formatEther(bridgeValue)} ETH`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + if (bridgeValue < arbFee) { + console.warn(` Warning: bridgeValue (${ethers.formatEther(bridgeValue)}) may be below Arbitrum bridge cost (${ethers.formatEther(arbFee)})`); + } + + await ensureRouterNativeBalance(signer, ROUTER_ETH); + + const exec = new ModularActionsBuilder(); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + // Native ETH input — send directly to the router; no AllowanceHolder needed. + console.log('Sending direct router tx → router.performModularExecution...'); + const receipt = await execDirect(signer, ROUTER_ETH, callData, inputAmount); + + logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — performModularExecution preFee', CHAIN_IDS.ETHEREUM, receipt); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); + + void arbFee; +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/bridge.preFee.ts b/scripts/e2e/cctp/bridge.preFee.ts new file mode 100644 index 0000000..196daf6 --- /dev/null +++ b/scripts/e2e/cctp/bridge.preFee.ts @@ -0,0 +1,118 @@ +/** + * Route: Polygon USDC → Base USDC (CCTP v2, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in depositForBurn calldata (no splice needed). + * Uses router.bridge() rather than performExecution / performModularExecution. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, + amount: bigint, + fastPath: boolean = true, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); + const maxFee = fastPath ? 1_000_000n : 0n; + const minFinalityThreshold = fastPath ? 1000 : 2000; + return iface.encodeFunctionData('depositForBurn', [ + amount, + destinationCctpDomain, + mintRecipient, + burnToken, + ethers.ZeroHash, + maxFee, + minFinalityThreshold, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon. Fund ${inputToken} on Polygon PoS.`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Input USDC: ${ethers.formatUnits(inputAmount, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + const depositData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + bridgeAmount, + ); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, polyCctp.tokenMessenger); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — CCTP — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/bridge.preFee.ts b/scripts/e2e/oft/bridge.preFee.ts new file mode 100644 index 0000000..e1f1033 --- /dev/null +++ b/scripts/e2e/oft/bridge.preFee.ts @@ -0,0 +1,160 @@ +/** + * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDT0 deducted before bridge + * + * Bridge amount is pre-encoded in OFT send() calldata (no splice needed). + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performModularExecution. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +/** + * Builds OFT send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildOftSendCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDT0_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const sendData = buildOftSendCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, USDT0_OFT_ADAPTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary('Polygon USDT0 → Arbitrum USDT0 (OFT) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/aave.bridge.preFee.ts b/scripts/e2e/relay/aave.bridge.preFee.ts new file mode 100644 index 0000000..15efa33 --- /dev/null +++ b/scripts/e2e/relay/aave.bridge.preFee.ts @@ -0,0 +1,97 @@ +/** + * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge + * + * Bridge amount is pre-encoded in Relay deposit calldata. + * Uses router.bridge() rather than performExecution / performModularExecution. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 18)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon AAVE → Base AAVE — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/aave.performExecution.preFee.ts b/scripts/e2e/relay/aave.performExecution.preFee.ts new file mode 100644 index 0000000..ea6ea8c --- /dev/null +++ b/scripts/e2e/relay/aave.performExecution.preFee.ts @@ -0,0 +1,112 @@ +/** + * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) + * Function: performExecution (monolithic) + * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge + * + * Fetches a Relay.link /quote/v2 for the net bridge amount, then encodes a + * MonolithicExecutionCall with preFee and the deposit calldata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + ZERO_BYTES32, + monolithicArgs, +} from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 18)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken, inputAmount }, + preFee: { receiver: signerAddress, amount: feeAmount }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { target: depositTarget, approvalSpender: relaySpender, value: 0n }, + flags: 0n, + }, + swapCallData: '0x', + bridgeCallData: depositData, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.performExecution...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon AAVE → Base AAVE — Relay — performExecution preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/aave.performModularExecution.preFee.ts b/scripts/e2e/relay/aave.performModularExecution.preFee.ts new file mode 100644 index 0000000..eaa5dd0 --- /dev/null +++ b/scripts/e2e/relay/aave.performModularExecution.preFee.ts @@ -0,0 +1,107 @@ +/** + * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) + * Function: performModularExecution (modular) + * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.transfer(signer, feeAmount) — preFee out + * [2] AAVE.approve(relaySpender, bridgeAmount) + * [3] call(depositTarget, depositData) — Relay bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performModularExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 18)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); + exec.call(inputToken, encodeApprove(relaySpender, bridgeAmount)); + exec.call(depositTarget, depositData); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.performModularExecution...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon AAVE → Base AAVE — Relay — performModularExecution preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/usdc.bridge.preFee.ts b/scripts/e2e/relay/usdc.bridge.preFee.ts new file mode 100644 index 0000000..3b374b6 --- /dev/null +++ b/scripts/e2e/relay/usdc.bridge.preFee.ts @@ -0,0 +1,97 @@ +/** + * Route: Polygon USDC → Base USDC via Relay.link (no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in Relay deposit calldata. + * Uses router.bridge() rather than performExecution / performModularExecution. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/usdc.performExecution.preFee.ts b/scripts/e2e/relay/usdc.performExecution.preFee.ts new file mode 100644 index 0000000..9be5f2b --- /dev/null +++ b/scripts/e2e/relay/usdc.performExecution.preFee.ts @@ -0,0 +1,112 @@ +/** + * Route: Polygon USDC → Base USDC via Relay.link (no swap) + * Function: performExecution (monolithic) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Fetches a Relay.link /quote/v2 for the net bridge amount, then encodes a + * MonolithicExecutionCall with preFee and the deposit calldata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + MonolithicExecutionCall, + NO_FEE, + NO_SWAP, + ZERO_BYTES32, + monolithicArgs, +} from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const mono: MonolithicExecutionCall = { + exec: { + input: { user: signerAddress, inputToken, inputAmount }, + preFee: { receiver: signerAddress, amount: feeAmount }, + swap: NO_SWAP, + postFee: NO_FEE, + bridge: { target: depositTarget, approvalSpender: relaySpender, value: 0n }, + flags: 0n, + }, + swapCallData: '0x', + bridgeCallData: depositData, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.performExecution...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — Relay — performExecution preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts new file mode 100644 index 0000000..66cbc85 --- /dev/null +++ b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts @@ -0,0 +1,107 @@ +/** + * Route: Polygon USDC → Base USDC via Relay.link (no swap) + * Function: performModularExecution (modular) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.transfer(signer, feeAmount) — preFee out + * [2] USDC.approve(relaySpender, bridgeAmount) + * [3] call(depositTarget, depositData) — Relay bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performModularExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeApprove, encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); + exec.call(inputToken, encodeApprove(relaySpender, bridgeAmount)); + exec.call(depositTarget, depositData); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.performModularExecution...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — Relay — performModularExecution preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts new file mode 100644 index 0000000..5483846 --- /dev/null +++ b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts @@ -0,0 +1,158 @@ +/** + * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in Stargate send() calldata (no splice needed). + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performModularExecution. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + STARGATE_USDC_POLYGON, + BASE_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +/** + * Builds Stargate send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildStargateCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); + + const sendData = buildStargateCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { + target: STARGATE_USDC_POLYGON, + approvalSpender: STARGATE_USDC_POLYGON, + value: nativeFeeWithBuffer, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, STARGATE_USDC_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary('Polygon USDC → Base USDC (Stargate USDC pool) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log('\nUSDC arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From d24bb5a90d7d4e23d463a6ecaff380fd18692f6d Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 12:57:39 +0530 Subject: [PATCH 37/69] fix: swap receiver --- src/combined/BungeeOpenRouterV2Unchecked.sol | 48 ++++++++++++++------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 729699c..a44240e 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -214,6 +214,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /** * @notice Pull → optional pre/post fee → swap. * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. + * @param receiver Address that ultimately receives the swap output (net of any post-swap fee). + * For pre-fee / no-fee: the swap router must be instructed (via `swapCallData`) to send + * tokens directly to `receiver`; the contract never holds the output. + * For post-fee: tokens land at this contract, fee is deducted, net is forwarded to `receiver`. * @param flags Packed flags; OR masks then test with `flags & MASK != 0`. Masks: `FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` (0x02). * Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), * `2` (pre-fee, balance delta), `3` (post-fee, balance delta). @@ -226,12 +230,16 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { function swap( bytes32 requestHash, InputData calldata input, + address receiver, uint256 flags, FeeData calldata fee, SwapData calldata swapData, bytes calldata swapCallData ) external payable returns (uint256 finalAmount) { - if (input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0)) { + if ( + input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0) + || receiver == address(0) + ) { revert InvalidExecution(); } @@ -259,11 +267,15 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); } + // Post-fee: swap output lands at this contract so the fee can be deducted before forwarding. + // Pre-fee / no-fee: swap calldata encodes `receiver` as the output recipient; tokens never touch this contract. + address outputReceiver = postFee ? address(this) : receiver; + // BALANCE_FLAG_BIT_MASK: unset ⇒ decode output word from returndata; set ⇒ output = delta on outputToken - finalAmount = _execSwap(swapData, swapCallData, flags & BALANCE_FLAG_BIT_MASK != 0); + finalAmount = _execSwap(swapData, swapCallData, flags & BALANCE_FLAG_BIT_MASK != 0, outputReceiver); if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); - // collect post-swap fee + // collect post-swap fee and forward net to receiver if (postFee) { uint256 feeAmount = fee.amount; if (feeAmount > finalAmount) revert InsufficientFunds(); @@ -271,7 +283,10 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { unchecked { finalAmount -= feeAmount; } + // Tokens are at this contract; transfer net output to receiver + CurrencyLib.transfer(swapData.outputToken, receiver, finalAmount); } + // Pre-fee / no-fee: tokens were sent directly to `receiver` by the swap router; nothing to transfer emit RequestExecuted(requestHash); } @@ -340,8 +355,9 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` + // Swap output always lands at this contract regardless of fee timing — tokens must be here for bridging. bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; - finalAmount = _execSwap(swapData, swapCallData, useBalanceOf); + finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, address(this)); if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); if (postFee) { @@ -508,7 +524,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // Monolithic path: only `BALANCE_FLAG_BIT_MASK` is read for `_execSwap`; fee uses `preFee` / `postFee`, not bit 0. - finalAmount = _execSwap(exec.swap, swapCallData, exec.flags & BALANCE_FLAG_BIT_MASK != 0); + // Swap output always lands at this contract; it feeds directly into the bridge step. + finalAmount = _execSwap(exec.swap, swapCallData, exec.flags & BALANCE_FLAG_BIT_MASK != 0, address(this)); if (finalAmount < exec.swap.minOutput) revert SwapOutputInsufficient(); finalToken = exec.swap.outputToken; } @@ -518,17 +535,22 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // ========================================================================= /// @dev Execute swap; output measured via returndata word or output-token balance delta. - /// useBalanceOf=true: measure output as (balance after - balance before). + /// useBalanceOf=true: measure output as (balance after - balance before) at `outputReceiver`. /// useBalanceOf=false: decode output from returndata at swapData.returnDataWordOffset. - function _execSwap(SwapData calldata swapData, bytes calldata swapCallData, bool useBalanceOf) - internal - returns (uint256 finalAmount) - { + /// `outputReceiver` must be `address(this)` when tokens are expected at the contract + /// (post-swap fee path, bridge path) or `user` when the swap router sends directly to them + /// (pre-swap fee / no-fee standalone swap). + function _execSwap( + SwapData calldata swapData, + bytes calldata swapCallData, + bool useBalanceOf, + address outputReceiver + ) internal returns (uint256 finalAmount) { if (useBalanceOf) { - // Balance delta mode: snapshot before, call, measure delta - uint256 before = CurrencyLib.balanceOf(swapData.outputToken, address(this)); + // Balance delta mode: snapshot before, call, measure delta at the expected recipient + uint256 before = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver); _doCallCalldata(swapData.target, swapData.value, swapCallData, false); - finalAmount = CurrencyLib.balanceOf(swapData.outputToken, address(this)) - before; + finalAmount = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver) - before; } else { // Returndata mode: decode output from a specific word in returndata bytes memory ret = _doCallCalldata(swapData.target, swapData.value, swapCallData, true); From 22c39b6fd4b73b6746b140e25b18c9926c6db331 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 17:06:27 +0530 Subject: [PATCH 38/69] fix: sum amount + bd.value --- src/combined/BungeeOpenRouterV2Unchecked.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index a44240e..8bc6b8c 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -577,7 +577,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // when set, forward amount as msg.value for native-token bridges - uint256 bridgeValue = flags & BRIDGE_VALUE_FLAG_BIT_MASK != 0 ? amount : bd.value; + uint256 bridgeValue = flags & BRIDGE_VALUE_FLAG_BIT_MASK != 0 ? amount + bd.value : bd.value; _doCall(bd.target, bridgeValue, bData); } From 2dc00802a20a292e876c7ebdfd22d94dde868b02 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 17:15:59 +0530 Subject: [PATCH 39/69] fix: comments --- src/combined/BungeeOpenRouterV2Unchecked.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 8bc6b8c..6445c6b 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -68,8 +68,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { SwapData swap; FeeData postFee; BridgeData bridge; - /// Packed flags; monolithic pipeline tests `BALANCE_FLAG_BIT_MASK` in `_execSwap` - /// and `BRIDGE_VALUE_FLAG_BIT_MASK` in `_doBridge`. + /// Packed flags; monolithic pipeline tests `BALANCE_FLAG_BIT_MASK` in `_execSwap`, + /// `BRIDGE_VALUE_FLAG_BIT_MASK` and `BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK` in `_doBridge`. /// Fee timing uses `preFee` / `postFee` structs — `FEE_FLAG_BIT_MASK` (bit 0) is ignored here. uint256 flags; } @@ -102,7 +102,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // bits 31..16 : bridge amount word byte offset, uint16, used only when bit 3 is set // bits 15..4 : reserved (0) // bit 3 : BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK (0x08) — splice finalAmount into bridge calldata - // bit 2 : BRIDGE_VALUE_FLAG_BIT_MASK (0x04) — bridge msg.value: static value vs final amount + // bit 2 : BRIDGE_VALUE_FLAG_BIT_MASK (0x04) — bridge msg.value: bridge.value alone vs finalAmount + bridge.value // bit 1 : BALANCE_FLAG_BIT_MASK (0x02) — swap output: returndata vs balance delta // bit 0 : FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap (standalone paths only) // @@ -114,7 +114,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // 0x01 00000001 yes returndata word bridge.value // 0x02 00000010 no balance delta on outputToken bridge.value // 0x03 00000011 yes balance delta on outputToken bridge.value - // 0x04 00000100 no returndata word finalAmount + // 0x04 00000100 no returndata word finalAmount + bridge.value // // FEE_FLAG_BIT_MASK selects bit 0 — fee timing. // Cleared — pull → deduct fee from input token → swap remainder. @@ -125,8 +125,8 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Set — snapshot outputToken balance before call, measure (after − before) as output. // // BRIDGE_VALUE_FLAG_BIT_MASK selects bit 2 — bridge native value source. - // Cleared — forward `bridge.value`. - // Set — forward finalAmount as msg.value. + // Cleared — forward `bridge.value` as msg.value. + // Set — forward `finalAmount + bridge.value` as msg.value (bridge.value carries static addend, e.g. LZ nativeFee). // // BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK selects bit 3 — bridge calldata amount splicing. // Cleared — no runtime amount splice. @@ -140,7 +140,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { /// @dev Bit mask 0x02: measure swap output by balance delta when `(flags & mask) != 0`; clear = returndata word. uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; - /// @dev Bit mask 0x04: bridge.value is ignored and finalAmount is forwarded as msg.value. + /// @dev Bit mask 0x04: `finalAmount + bridge.value` is forwarded as msg.value (bridge.value acts as a static addend, e.g. LZ nativeFee). uint256 internal constant BRIDGE_VALUE_FLAG_BIT_MASK = 0x04; /// @dev Bit mask 0x08: splice finalAmount into bridge calldata at the uint16 position packed in flags. From c88af75b1757fffb777367899c9c0f2481a4cb42 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 17:16:09 +0530 Subject: [PATCH 40/69] test: fix stargate tests --- ...arbUsdcBaseEth.performExecution.postFee.ts | 18 +++++----- ...baseUsdcArbEth.performExecution.postFee.ts | 15 ++++---- .../swapAndBridge.postFee.balanceOf.ts | 24 ++++++------- .../swapAndBridge.postFee.returndata.ts | 25 ++++++-------- .../swapAndBridge.preFee.balanceOf.ts | 22 ++++++------ .../swapAndBridge.preFee.returndata.ts | 23 ++++++------- scripts/e2e/swapBridgeViaArbitrumNative.ts | 2 +- scripts/e2e/swapBridgeViaStargateNative.ts | 34 ++++++++----------- 8 files changed, 77 insertions(+), 86 deletions(-) diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts index df2b82e..0ded523 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -3,9 +3,9 @@ * Function: performExecution (monolithic) * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap * - * BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to Stargate. - * amountLD = minAmountOut - fee - nativeFeeWithBuffer (pre-encoded in calldata). - * StargatePoolNative check: msg.value >= amountLD + nativeFee; satisfied since actual >= min. + * BRIDGE_VALUE_FLAG set: router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BRIDGE_AMOUNT_POSITION_FLAG set: router splices finalETH into amountLD at runtime. + * Stargate receives the exact actual post-swap, post-fee ETH as amountLD. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -39,6 +39,7 @@ import { ZERO_ADDRESS, ZERO_BYTES32, monolithicArgs, + bridgeAmountPositionFlag, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; @@ -142,9 +143,8 @@ async function main() { console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - // amountLD pre-encoded: minAmountOut - fee - nativeFeeWithBuffer - const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; - if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + // estimatedBridgeAmount is a placeholder; router splices the actual finalETH at runtime + const amountLD = estimatedBridgeAmount; await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); @@ -165,8 +165,8 @@ async function main() { returnDataWordOffset: 0n, }, postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, value: 0n }, - flags: BRIDGE_VALUE_FLAG, + bridge: { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer }, + flags: BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), }, swapCallData: swapData, bridgeCallData: stargateData, @@ -184,8 +184,6 @@ async function main() { ); console.log('\nETH arrives on Base once LZ delivers the message.'); - - void STARGATE_AMOUNT_LD_OFFSET; } main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts index ac1647b..081a259 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -3,8 +3,9 @@ * Function: performExecution (monolithic) * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap * - * BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to Stargate. - * amountLD = minAmountOut - fee - nativeFeeWithBuffer (pre-encoded in calldata). + * BRIDGE_VALUE_FLAG set: router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BRIDGE_AMOUNT_POSITION_FLAG set: router splices finalETH into amountLD at runtime. + * Stargate receives the exact actual post-swap, post-fee ETH as amountLD. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -26,6 +27,7 @@ import { NATIVE_TOKEN_ADDRESS, STARGATE_NATIVE_BASE, ARBITRUM_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; import { getWalletErc20Balance } from '../utils/erc20'; @@ -37,6 +39,7 @@ import { ZERO_ADDRESS, ZERO_BYTES32, monolithicArgs, + bridgeAmountPositionFlag, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; @@ -138,8 +141,8 @@ async function main() { console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; - if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + // estimatedBridgeAmount is a placeholder; router splices the actual finalETH at runtime + const amountLD = estimatedBridgeAmount; await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); @@ -160,8 +163,8 @@ async function main() { returnDataWordOffset: 0n, }, postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: 0n }, - flags: BRIDGE_VALUE_FLAG, + bridge: { target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer }, + flags: BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), }, swapCallData: swapData, bridgeCallData: stargateData, diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts index f65ba2f..e9c8e90 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -1,13 +1,13 @@ /** * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) * Flags: post-fee (fee taken from ETH output after swap), output measured as ETH balanceOf delta - * bridge-value flag: router forwards finalETH as msg.value to Stargate + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate * * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. * BalanceOf (bit1=1): final ETH amount is measured as router ETH balance change (not returndata). - * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). - * - * amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor). + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -40,7 +40,9 @@ import { ZERO_BYTES32, BRIDGE_VALUE_FLAG, ZERO_ADDRESS, + bridgeAmountPositionFlag, } from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -48,8 +50,8 @@ import { ensureRouterApproval, } from "../utils/reproducibility"; -// post-fee (0x01) | balance-of (0x02) | bridge-value (0x04): forward finalETH as msg.value -const FLAGS = 0x03n | BRIDGE_VALUE_FLAG; +// post-fee (0x01) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) +const FLAGS = 0x03n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); const STARGATE_ABI = [ @@ -194,12 +196,8 @@ async function main() { console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - // amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor) - const amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer; - if (amountLD <= 0n) - throw new Error( - "estimatedOut too small to cover fee + nativeFeeWithBuffer" - ); + // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = bridgeEstimate; await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); @@ -232,7 +230,7 @@ async function main() { { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, - value: 0n, + value: nativeFeeWithBuffer, }, stargateData, ]); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts index fc7d448..6218e6f 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -1,14 +1,13 @@ /** * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) * Flags: post-fee (fee taken from ETH output after swap), output read from swap returndata word 0 - * bridge-value flag: router forwards finalETH as msg.value to Stargate + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate * * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. - * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). - * - * amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor). - * StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= minAmountOut. + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -41,7 +40,9 @@ import { ZERO_BYTES32, BRIDGE_VALUE_FLAG, ZERO_ADDRESS, + bridgeAmountPositionFlag, } from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -49,8 +50,8 @@ import { ensureRouterApproval, } from "../utils/reproducibility"; -// post-fee (0x01) | bridge-value (0x04): forward finalETH as msg.value -const FLAGS = 0x01n | BRIDGE_VALUE_FLAG; +// post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee +const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); const STARGATE_ABI = [ @@ -196,12 +197,8 @@ async function main() { console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - // amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer (conservative floor) - const amountLD = estimatedOut - feeAmount - nativeFeeWithBuffer; - if (amountLD <= 0n) - throw new Error( - "estimatedOut too small to cover fee + nativeFeeWithBuffer" - ); + // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = bridgeEstimate; await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); @@ -234,7 +231,7 @@ async function main() { { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, - value: 0n, + value: nativeFeeWithBuffer, }, stargateData, ]); diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts index 069fd53..789b18b 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -1,13 +1,13 @@ /** * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) * Flags: pre-fee (fee taken from USDC input before swap), output measured as ETH balanceOf delta - * bridge-value flag: router forwards finalETH as msg.value to Stargate + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate * * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount USDC, deducted before the swap. * BalanceOf (bit1=1): final ETH amount is measured as router ETH balance change (not returndata). - * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). - * - * amountLD = estimatedOut - nativeFeeWithBuffer (conservative floor; actual >= amountLD). + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -40,7 +40,9 @@ import { ZERO_BYTES32, BRIDGE_VALUE_FLAG, ZERO_ADDRESS, + bridgeAmountPositionFlag, } from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -48,8 +50,8 @@ import { ensureRouterApproval, } from "../utils/reproducibility"; -// pre-fee (0x00) | balance-of (0x02) | bridge-value (0x04): forward finalETH as msg.value -const FLAGS = 0x02n | BRIDGE_VALUE_FLAG; +// pre-fee (0x00) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) +const FLAGS = 0x02n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); const STARGATE_ABI = [ @@ -194,10 +196,8 @@ async function main() { console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - // amountLD = estimatedOut - nativeFeeWithBuffer (conservative floor) - const amountLD = estimatedOut - nativeFeeWithBuffer; - if (amountLD <= 0n) - throw new Error("estimatedOut too small to cover nativeFeeWithBuffer"); + // estimatedOut is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = estimatedOut; await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); @@ -230,7 +230,7 @@ async function main() { { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, - value: 0n, + value: nativeFeeWithBuffer, }, stargateData, ]); diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts index c897ee8..c412fdb 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -1,14 +1,13 @@ /** * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) * Flags: pre-fee (fee taken from USDC input before swap), output read from swap returndata word 0 - * bridge-value flag: router forwards finalETH as msg.value to Stargate + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate * * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount USDC, deducted before the swap. * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. - * BridgeValue (bit2=1): router forwards finalETH as msg.value to Stargate send(). - * - * amountLD is pre-encoded conservatively as estimatedOut - nativeFeeWithBuffer. - * StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= amountLD + buffer. + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -41,7 +40,9 @@ import { ZERO_BYTES32, BRIDGE_VALUE_FLAG, ZERO_ADDRESS, + bridgeAmountPositionFlag, } from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -49,8 +50,8 @@ import { ensureRouterApproval, } from "../utils/reproducibility"; -// pre-fee (0x00) | bridge-value (0x04): forward finalETH as msg.value -const FLAGS = BRIDGE_VALUE_FLAG; +// pre-fee (0x00) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee +const FLAGS = BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); const STARGATE_ABI = [ @@ -195,10 +196,8 @@ async function main() { console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - // amountLD = estimatedOut - nativeFeeWithBuffer (conservative floor for pre-encoded calldata) - const amountLD = estimatedOut - nativeFeeWithBuffer; - if (amountLD <= 0n) - throw new Error("estimatedOut too small to cover nativeFeeWithBuffer"); + // estimatedOut is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = estimatedOut; await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); @@ -231,7 +230,7 @@ async function main() { { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, - value: 0n, + value: nativeFeeWithBuffer, }, stargateData, ]); diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts index 0b0236c..1dce431 100644 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ b/scripts/e2e/swapBridgeViaArbitrumNative.ts @@ -239,7 +239,7 @@ function buildMonolithic( bridge: { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, - value: 0n, // ignored when BRIDGE_VALUE_FLAG is set + value: 0n, // no addend: bridgeValue = finalETH + 0 = finalETH }, flags: BRIDGE_VALUE_FLAG, }, diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts index 54961c5..2aa3aea 100644 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ b/scripts/e2e/swapBridgeViaStargateNative.ts @@ -9,10 +9,10 @@ * * Native-pool mechanics (cases 1 & 3): * send() requires msg.value >= amountLD + nativeFee (StargatePoolNative._assertMessagingFee). - * Monolithic: BRIDGE_VALUE_FLAG set (router forwards actualFinalAmount as msg.value). - * amountLD = minAmountOut - fee - nativeFeeWithBuffer; no splice flag. - * Since actual >= min (OO slippage), msg.value >= amountLD + nativeFeeWithBuffer ✓ - * Modular: amountLD = minAmountOut - fee - nativeFeeWithBuffer (same). + * Monolithic: BRIDGE_VALUE_FLAG + BRIDGE_AMOUNT_POSITION_FLAG set. + * Router splices finalETH into amountLD at runtime; msg.value = finalETH + nativeFeeWithBuffer. + * finalETH + nativeFeeWithBuffer >= finalETH + nativeFee ✓; destination gets exact finalETH. + * Modular: amountLD = minAmountOut - fee - nativeFeeWithBuffer (static; no splice available). * nativeCall Stargate with value = amountLD + nativeFeeWithBuffer = minAmountOut - fee. * * ERC20-pool mechanics (case 2): @@ -484,9 +484,10 @@ function buildStargateCalldata( /** * Monolithic for native-pool cases (cases 1 & 3): * - OO swap input token → native ETH - * - BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to Stargate - * - amountLD = minAmountOut - fee - nativeFeeWithBuffer; pre-encoded; no splice flag needed - * - StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since actual >= min + * - BRIDGE_VALUE_FLAG + BRIDGE_AMOUNT_POSITION_FLAG: router splices finalETH into amountLD at + * runtime and forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate + * - StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since + * finalETH + nativeFeeWithBuffer >= finalETH + nativeFee ✓ */ function buildNativePoolMonolithic( signer: string, @@ -497,6 +498,7 @@ function buildNativePoolMonolithic( ooRouter: string, swapData: string, stargateData: string, + nativeFeeWithBuffer: bigint, ): MonolithicExecutionCall { return { exec: { @@ -514,9 +516,9 @@ function buildNativePoolMonolithic( bridge: { target: cfg.bridgeContract, approvalSpender: ZERO_ADDRESS, // no ERC20 approval for native ETH - value: 0n, // ignored when BRIDGE_VALUE_FLAG is set + value: nativeFeeWithBuffer, // added to finalETH as msg.value by BRIDGE_VALUE_FLAG }, - flags: BRIDGE_VALUE_FLAG, + flags: BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), }, swapCallData: swapData, bridgeCallData: stargateData, @@ -944,15 +946,9 @@ async function executeLeg( } // ──────────────────────────────────────────────────────────────────────────── - let amountLD: bigint; - if (cfg.isNativePool) { - amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; - if (amountLD <= 0n) { - throw new Error(`${cfg.name}: minAmountOut too small to cover fee + nativeFee.`); - } - } else { - amountLD = 0n; - } + // Native pool: use estimatedBridgeAmount as placeholder; router splices actual finalETH at runtime. + // ERC20 pool: 0n placeholder; router splices actual post-fee balance at runtime. + const amountLD = cfg.isNativePool ? estimatedBridgeAmount : 0n; const stargateData = buildStargateCalldata(cfg.destLzEid, nativeFeeWithBuffer, signerAddress, amountLD, cfg.lzExtraOptions); @@ -980,7 +976,7 @@ async function executeLeg( if (cfg.isNativePool) { mono = buildNativePoolMonolithic( signerAddress, cfg, inputAmountWei, feeAmount, minAmountOut, - ooRouter, swapData, stargateData, + ooRouter, swapData, stargateData, nativeFeeWithBuffer, ); } else if (cfg.isNativeInput) { mono = buildNativeInErc20BridgeMonolithic( From 29ab09904b8ad9c5a6da127f736fe81a7300f161 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 17:58:34 +0530 Subject: [PATCH 41/69] fix: stargate tests --- ...arbUsdcBaseEth.performExecution.postFee.ts | 2 +- ...BaseEth.performModularExecution.postFee.ts | 2 +- ...baseUsdcArbEth.performExecution.postFee.ts | 2 +- ...cArbEth.performModularExecution.postFee.ts | 2 +- ...olygonUsdcBase.performExecution.postFee.ts | 2 +- ...sdcBase.performModularExecution.postFee.ts | 2 +- .../swapAndBridge.postFee.balanceOf.ts | 58 +++++++++---------- .../swapAndBridge.postFee.returndata.ts | 58 +++++++++---------- .../swapAndBridge.preFee.balanceOf.ts | 58 +++++++++---------- .../swapAndBridge.preFee.returndata.ts | 58 +++++++++---------- 10 files changed, 122 insertions(+), 122 deletions(-) diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts index 0ded523..198a451 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -175,7 +175,7 @@ async function main() { const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); - const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData); + const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData, nativeFeeWithBuffer); logTxnSummary( 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performExecution postFee', diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts index 3f301dd..49c6a92 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts @@ -164,7 +164,7 @@ async function main() { const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); - const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData); + const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData, nativeFeeWithBuffer); logTxnSummary( 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performModularExecution postFee', diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts index 081a259..fa2f89d 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -173,7 +173,7 @@ async function main() { const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); - const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData); + const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData, nativeFeeWithBuffer); logTxnSummary( 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performExecution postFee', diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts index 7eb6f04..d26740e 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts @@ -160,7 +160,7 @@ async function main() { const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); - const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData); + const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData, nativeFeeWithBuffer); logTxnSummary( 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performModularExecution postFee', diff --git a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts index f1de77b..50c0083 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -121,7 +121,7 @@ async function main() { const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); - const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); logTxnSummary( 'Polygon USDC → Base USDC (Stargate USDC pool) — performExecution postFee', diff --git a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts index 87dd89b..7ecf31e 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts @@ -119,7 +119,7 @@ async function main() { const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); - const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); logTxnSummary( 'Polygon USDC → Base USDC (Stargate USDC pool) — performModularExecution postFee', diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts index e9c8e90..46c549f 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -1,5 +1,5 @@ /** - * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) * Flags: post-fee (fee taken from ETH output after swap), output measured as ETH balanceOf delta * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate @@ -27,8 +27,8 @@ import { RPC, OPEN_OCEAN_API_KEY, OO_SLIPPAGE_PERCENT, - STARGATE_NATIVE_ARB, - BASE_LZ_EID, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, } from "../config"; import { execViaAH, @@ -52,7 +52,7 @@ import { // post-fee (0x01) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) const FLAGS = 0x03n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); -const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); const STARGATE_ABI = [ "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", @@ -73,16 +73,16 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ minAmountOut: bigint; }> { const params: Record = { - inTokenAddress: TOKENS.USDC_ARB, + inTokenAddress: TOKENS.USDC_BASE, outTokenAddress: NATIVE_TOKEN_ADDRESS, amount: ethers.formatUnits(inputAmount, 6), slippage: OO_SLIPPAGE_PERCENT, - sender: ROUTER_ARB, - account: ROUTER_ARB, + sender: ROUTER_BASE, + account: ROUTER_BASE, gasPrice: "1", }; if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; const response = await axios.get(url, { params }); const q = response.data.data; return { @@ -99,13 +99,13 @@ async function fetchStargateQuote( recipient: string ): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract( - STARGATE_NATIVE_ARB, + STARGATE_NATIVE_BASE, STARGATE_ABI, provider ); const to32 = ethers.zeroPadValue(recipient, 32); const sendParam = { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, @@ -130,7 +130,7 @@ function buildStargateCalldata( ): string { return STARGATE_IFACE.encodeFunctionData("send", [ { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, @@ -147,23 +147,23 @@ async function main() { const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new Error("PRIVATE_KEY env var required"); - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const provider = new ethers.JsonRpcProvider(RPC.BASE); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); const { balance: walletBalance } = await getWalletErc20Balance( - TOKENS.USDC_ARB, + TOKENS.USDC_BASE, signerAddress, provider ); if (walletBalance === 0n) - throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); const inputAmount = walletBalance - 20n; if (inputAmount === 0n) throw new Error("Balance too small"); console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ARB}`); + console.log(`Router: ${ROUTER_BASE}`); console.log( `Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf, bridge-value)` ); @@ -186,7 +186,7 @@ async function main() { console.log(` Bridge est: ${ethers.formatEther(bridgeEstimate)}`); console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); - console.log("Fetching Stargate quote..."); + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); const { nativeFee, amountReceivedLD } = await fetchStargateQuote( provider, bridgeEstimate, @@ -199,9 +199,9 @@ async function main() { // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime const amountLD = bridgeEstimate; - await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); - await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -213,7 +213,7 @@ async function main() { ZERO_BYTES32, { user: signerAddress, - inputToken: TOKENS.USDC_ARB, + inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, FLAGS, @@ -228,31 +228,31 @@ async function main() { }, swapData, { - target: STARGATE_NATIVE_ARB, + target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer, }, stargateData, ]); - await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( signer, - ROUTER_ARB, - TOKENS.USDC_ARB, + ROUTER_BASE, + TOKENS.USDC_BASE, inputAmount, - ROUTER_ARB, + ROUTER_BASE, callData, - 0n + nativeFeeWithBuffer ); logTxnSummary( - `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge postFee/balanceOf`, - CHAIN_IDS.ARBITRUM, + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.BASE, receipt ); - console.log("\nETH arrives on Base once LZ delivers the message."); + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); } main().catch((err) => { diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts index 6218e6f..6ccdd7e 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -1,5 +1,5 @@ /** - * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) * Flags: post-fee (fee taken from ETH output after swap), output read from swap returndata word 0 * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate @@ -27,8 +27,8 @@ import { RPC, OPEN_OCEAN_API_KEY, OO_SLIPPAGE_PERCENT, - STARGATE_NATIVE_ARB, - BASE_LZ_EID, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, } from "../config"; import { execViaAH, @@ -52,7 +52,7 @@ import { // post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); -const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); const STARGATE_ABI = [ "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", @@ -73,16 +73,16 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ minAmountOut: bigint; }> { const params: Record = { - inTokenAddress: TOKENS.USDC_ARB, + inTokenAddress: TOKENS.USDC_BASE, outTokenAddress: NATIVE_TOKEN_ADDRESS, amount: ethers.formatUnits(inputAmount, 6), slippage: OO_SLIPPAGE_PERCENT, - sender: ROUTER_ARB, - account: ROUTER_ARB, + sender: ROUTER_BASE, + account: ROUTER_BASE, gasPrice: "1", }; if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; const response = await axios.get(url, { params }); const q = response.data.data; return { @@ -99,13 +99,13 @@ async function fetchStargateQuote( recipient: string ): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract( - STARGATE_NATIVE_ARB, + STARGATE_NATIVE_BASE, STARGATE_ABI, provider ); const to32 = ethers.zeroPadValue(recipient, 32); const sendParam = { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, @@ -130,7 +130,7 @@ function buildStargateCalldata( ): string { return STARGATE_IFACE.encodeFunctionData("send", [ { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, @@ -147,23 +147,23 @@ async function main() { const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new Error("PRIVATE_KEY env var required"); - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const provider = new ethers.JsonRpcProvider(RPC.BASE); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); const { balance: walletBalance } = await getWalletErc20Balance( - TOKENS.USDC_ARB, + TOKENS.USDC_BASE, signerAddress, provider ); if (walletBalance === 0n) - throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); const inputAmount = walletBalance - 20n; if (inputAmount === 0n) throw new Error("Balance too small"); console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ARB}`); + console.log(`Router: ${ROUTER_BASE}`); console.log( `Flags: 0x${FLAGS.toString( 16 @@ -186,7 +186,7 @@ async function main() { ); console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); - console.log("Fetching Stargate quote..."); + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); const bridgeEstimate = estimatedOut - feeAmount; const { nativeFee, amountReceivedLD } = await fetchStargateQuote( provider, @@ -200,9 +200,9 @@ async function main() { // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime const amountLD = bridgeEstimate; - await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); - await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -214,7 +214,7 @@ async function main() { ZERO_BYTES32, { user: signerAddress, - inputToken: TOKENS.USDC_ARB, + inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, FLAGS, @@ -229,31 +229,31 @@ async function main() { }, swapData, { - target: STARGATE_NATIVE_ARB, + target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer, }, stargateData, ]); - await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( signer, - ROUTER_ARB, - TOKENS.USDC_ARB, + ROUTER_BASE, + TOKENS.USDC_BASE, inputAmount, - ROUTER_ARB, + ROUTER_BASE, callData, - 0n + nativeFeeWithBuffer ); logTxnSummary( - `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge postFee/returndata`, - CHAIN_IDS.ARBITRUM, + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge postFee/returndata`, + CHAIN_IDS.BASE, receipt ); - console.log("\nETH arrives on Base once LZ delivers the message."); + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); } main().catch((err) => { diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts index 789b18b..8addf9b 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -1,5 +1,5 @@ /** - * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) * Flags: pre-fee (fee taken from USDC input before swap), output measured as ETH balanceOf delta * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate @@ -27,8 +27,8 @@ import { RPC, OPEN_OCEAN_API_KEY, OO_SLIPPAGE_PERCENT, - STARGATE_NATIVE_ARB, - BASE_LZ_EID, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, } from "../config"; import { execViaAH, @@ -52,7 +52,7 @@ import { // pre-fee (0x00) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) const FLAGS = 0x02n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); -const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); const STARGATE_ABI = [ "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", @@ -73,16 +73,16 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ minAmountOut: bigint; }> { const params: Record = { - inTokenAddress: TOKENS.USDC_ARB, + inTokenAddress: TOKENS.USDC_BASE, outTokenAddress: NATIVE_TOKEN_ADDRESS, amount: ethers.formatUnits(inputAmount, 6), slippage: OO_SLIPPAGE_PERCENT, - sender: ROUTER_ARB, - account: ROUTER_ARB, + sender: ROUTER_BASE, + account: ROUTER_BASE, gasPrice: "1", }; if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; const response = await axios.get(url, { params }); const q = response.data.data; return { @@ -99,13 +99,13 @@ async function fetchStargateQuote( recipient: string ): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract( - STARGATE_NATIVE_ARB, + STARGATE_NATIVE_BASE, STARGATE_ABI, provider ); const to32 = ethers.zeroPadValue(recipient, 32); const sendParam = { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, @@ -130,7 +130,7 @@ function buildStargateCalldata( ): string { return STARGATE_IFACE.encodeFunctionData("send", [ { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, @@ -147,23 +147,23 @@ async function main() { const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new Error("PRIVATE_KEY env var required"); - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const provider = new ethers.JsonRpcProvider(RPC.BASE); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); const { balance: walletBalance } = await getWalletErc20Balance( - TOKENS.USDC_ARB, + TOKENS.USDC_BASE, signerAddress, provider ); if (walletBalance === 0n) - throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); const inputAmount = walletBalance - 20n; if (inputAmount === 0n) throw new Error("Balance too small"); console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ARB}`); + console.log(`Router: ${ROUTER_BASE}`); console.log( `Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf, bridge-value)` ); @@ -186,7 +186,7 @@ async function main() { console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); - console.log("Fetching Stargate quote..."); + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); const { nativeFee, amountReceivedLD } = await fetchStargateQuote( provider, estimatedOut, @@ -199,9 +199,9 @@ async function main() { // estimatedOut is a placeholder for amountLD; router splices the actual finalETH at runtime const amountLD = estimatedOut; - await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); - await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -213,7 +213,7 @@ async function main() { ZERO_BYTES32, { user: signerAddress, - inputToken: TOKENS.USDC_ARB, + inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, FLAGS, @@ -228,31 +228,31 @@ async function main() { }, swapData, { - target: STARGATE_NATIVE_ARB, + target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer, }, stargateData, ]); - await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( signer, - ROUTER_ARB, - TOKENS.USDC_ARB, + ROUTER_BASE, + TOKENS.USDC_BASE, inputAmount, - ROUTER_ARB, + ROUTER_BASE, callData, - 0n + nativeFeeWithBuffer ); logTxnSummary( - `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge preFee/balanceOf`, - CHAIN_IDS.ARBITRUM, + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.BASE, receipt ); - console.log("\nETH arrives on Base once LZ delivers the message."); + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); } main().catch((err) => { diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts index c412fdb..0a2ee3a 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -1,5 +1,5 @@ /** - * Route: Arbitrum USDC → native ETH (OpenOcean) → Base ETH (Stargate Native Pool, LayerZero v2) + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) * Flags: pre-fee (fee taken from USDC input before swap), output read from swap returndata word 0 * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate @@ -27,8 +27,8 @@ import { RPC, OPEN_OCEAN_API_KEY, OO_SLIPPAGE_PERCENT, - STARGATE_NATIVE_ARB, - BASE_LZ_EID, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, } from "../config"; import { execViaAH, @@ -52,7 +52,7 @@ import { // pre-fee (0x00) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee const FLAGS = BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); -const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); const STARGATE_ABI = [ "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", @@ -73,16 +73,16 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ minAmountOut: bigint; }> { const params: Record = { - inTokenAddress: TOKENS.USDC_ARB, + inTokenAddress: TOKENS.USDC_BASE, outTokenAddress: NATIVE_TOKEN_ADDRESS, amount: ethers.formatUnits(inputAmount, 6), slippage: OO_SLIPPAGE_PERCENT, - sender: ROUTER_ARB, - account: ROUTER_ARB, + sender: ROUTER_BASE, + account: ROUTER_BASE, gasPrice: "1", }; if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; const response = await axios.get(url, { params }); const q = response.data.data; return { @@ -99,13 +99,13 @@ async function fetchStargateQuote( recipient: string ): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract( - STARGATE_NATIVE_ARB, + STARGATE_NATIVE_BASE, STARGATE_ABI, provider ); const to32 = ethers.zeroPadValue(recipient, 32); const sendParam = { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, @@ -130,7 +130,7 @@ function buildStargateCalldata( ): string { return STARGATE_IFACE.encodeFunctionData("send", [ { - dstEid: BASE_LZ_EID, + dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, @@ -147,23 +147,23 @@ async function main() { const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new Error("PRIVATE_KEY env var required"); - const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const provider = new ethers.JsonRpcProvider(RPC.BASE); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); const { balance: walletBalance } = await getWalletErc20Balance( - TOKENS.USDC_ARB, + TOKENS.USDC_BASE, signerAddress, provider ); if (walletBalance === 0n) - throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); const inputAmount = walletBalance - 20n; if (inputAmount === 0n) throw new Error("Balance too small"); console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_ARB}`); + console.log(`Router: ${ROUTER_BASE}`); console.log( `Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata, bridge-value)` ); @@ -186,7 +186,7 @@ async function main() { console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); - console.log("Fetching Stargate quote..."); + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); const { nativeFee, amountReceivedLD } = await fetchStargateQuote( provider, estimatedOut, @@ -199,9 +199,9 @@ async function main() { // estimatedOut is a placeholder for amountLD; router splices the actual finalETH at runtime const amountLD = estimatedOut; - await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); - await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -213,7 +213,7 @@ async function main() { ZERO_BYTES32, { user: signerAddress, - inputToken: TOKENS.USDC_ARB, + inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, FLAGS, @@ -228,31 +228,31 @@ async function main() { }, swapData, { - target: STARGATE_NATIVE_ARB, + target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer, }, stargateData, ]); - await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( signer, - ROUTER_ARB, - TOKENS.USDC_ARB, + ROUTER_BASE, + TOKENS.USDC_BASE, inputAmount, - ROUTER_ARB, + ROUTER_BASE, callData, - 0n + nativeFeeWithBuffer ); logTxnSummary( - `Arbitrum USDC → Base ETH (Stargate) — swapAndBridge preFee/returndata`, - CHAIN_IDS.ARBITRUM, + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge preFee/returndata`, + CHAIN_IDS.BASE, receipt ); - console.log("\nETH arrives on Base once LZ delivers the message."); + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); } main().catch((err) => { From 362b8be7293e113633e71a202a43a06104ff9ac2 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 20:20:49 +0530 Subject: [PATCH 42/69] feat: kyberswap, 0x swap scripts, fix swap scripts --- ...pAndBridge.postFee.returndata.kyberswap.ts | 273 ++++++++++++++++++ scripts/e2e/config.ts | 13 +- scripts/e2e/routerUsdc.withdraw.modular.ts | 103 +++++++ .../e2e/swap/kyberswap.postFee.balanceOf.ts | 232 +++++++++++++++ .../e2e/swap/kyberswap.postFee.returndata.ts | 227 +++++++++++++++ .../e2e/swap/kyberswap.preFee.balanceOf.ts | 238 +++++++++++++++ .../e2e/swap/kyberswap.preFee.returndata.ts | 234 +++++++++++++++ scripts/e2e/swap/swap.postFee.balanceOf.ts | 1 + scripts/e2e/swap/swap.postFee.returndata.ts | 1 + scripts/e2e/swap/swap.preFee.balanceOf.ts | 1 + scripts/e2e/swap/swap.preFee.returndata.ts | 1 + scripts/e2e/swap/zerox.postFee.balanceOf.ts | 228 +++++++++++++++ scripts/e2e/swap/zerox.postFee.returndata.ts | 217 ++++++++++++++ scripts/e2e/swap/zerox.preFee.balanceOf.ts | 235 +++++++++++++++ scripts/e2e/swap/zerox.preFee.returndata.ts | 227 +++++++++++++++ scripts/e2e/utils/routerAbi.ts | 1 + 16 files changed, 2230 insertions(+), 2 deletions(-) create mode 100644 scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts create mode 100644 scripts/e2e/routerUsdc.withdraw.modular.ts create mode 100644 scripts/e2e/swap/kyberswap.postFee.balanceOf.ts create mode 100644 scripts/e2e/swap/kyberswap.postFee.returndata.ts create mode 100644 scripts/e2e/swap/kyberswap.preFee.balanceOf.ts create mode 100644 scripts/e2e/swap/kyberswap.preFee.returndata.ts create mode 100644 scripts/e2e/swap/zerox.postFee.balanceOf.ts create mode 100644 scripts/e2e/swap/zerox.postFee.returndata.ts create mode 100644 scripts/e2e/swap/zerox.preFee.balanceOf.ts create mode 100644 scripts/e2e/swap/zerox.preFee.returndata.ts diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts new file mode 100644 index 0000000..b09b480 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts @@ -0,0 +1,273 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) → Base USDC (CCTP depositForBurn) + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x01n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Post-fee Kyber build: sender and recipient are the router so gross USDC stays on-contract + * before fee deduction and CCTP burn (same net shape as the OpenOcean post-fee script). + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=4)`, + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(inputAmount, ROUTER_POLYGON); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: ksRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: polyCctp.tokenMessenger, + value: 0n, + }, + depositForBurnData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee/returndata (Kyber)`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 6e6518c..a285251 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -34,9 +34,9 @@ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. */ export const ROUTER_BY_CHAIN_ID: Record = { - [CHAIN_IDS.POLYGON]: '0x7A113007177BF1cd86da69Dbd7d601dcEC9EbAbD', + [CHAIN_IDS.POLYGON]: '0x5abf9dccabc44ea9421f1e1Fbd6BA6A4f2387342', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', - [CHAIN_IDS.BASE]: '0x96E8c261fCCDFca2CCffe8b4A33dC8a65f153785', + [CHAIN_IDS.BASE]: '0x91b536E79cd3607b593f3011937862609D608253', [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', }; @@ -236,3 +236,12 @@ export const RPC = { export const RELAY_API_KEY: string | undefined = process.env.RELAY_API_KEY; export const OPEN_OCEAN_API_KEY: string | undefined = process.env.OPEN_OCEAN_API_KEY; +export const KYBERSWAP_API_KEY: string | undefined = + process.env.KYBERSWAP_API_KEY; +export const ZEROX_API_KEY: string | undefined = process.env.ZEROX_API_KEY; + +/** + * Swap slippage in basis points for KyberSwap and 0x (300 = 3%). + * Matches the default OO_SLIPPAGE_PERCENT of 3%. + */ +export const SWAP_SLIPPAGE_BPS = 300; diff --git a/scripts/e2e/routerUsdc.withdraw.modular.ts b/scripts/e2e/routerUsdc.withdraw.modular.ts new file mode 100644 index 0000000..210307d --- /dev/null +++ b/scripts/e2e/routerUsdc.withdraw.modular.ts @@ -0,0 +1,103 @@ +/** + * Polygon: sweep USDC from `BungeeOpenRouterV2Unchecked` to the tx sender using + * `performModularExecution` only — no AllowanceHolder, no pull step. + * + * Actions: + * [0] STATICCALL USDC.balanceOf(router) — stored returndata (32-byte uint256) + * [1] CALL USDC.transfer(caller, 0) — amount word spliced from [0], so net effect + * is transferring the router's entire USDC balance to `msg.sender` of this tx. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/polygon/routerUsdc.withdraw.modular.ts + * + * Requires the router contract to actually hold Polygon USDC + * ({@link TOKENS.USDC_POLYGON_CIRCLE}). + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { CHAIN_IDS, routerAddressForChain, TOKENS, RPC } from './config'; +import { + encodeBalanceOf, + encodeTransfer, + getWalletErc20Balance, +} from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from './utils/contractTypes'; +import { logTxnSummary } from './utils/txnLogSummary'; + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const chainId = CHAIN_IDS.POLYGON; + const rpcUrl = process.env.POLYGON_RPC ?? process.env.RPC_URL ?? RPC.POLYGON; + const routerAddress = routerAddressForChain(chainId); + const usdc = TOKENS.USDC_POLYGON_CIRCLE; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: routerBalance } = await getWalletErc20Balance( + usdc, + routerAddress, + provider, + ); + if (routerBalance === 0n) { + throw new Error(`Router ${routerAddress} holds zero USDC on Polygon`); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${routerAddress}`); + console.log(`Router USDC bal: ${ethers.formatUnits(routerBalance, 6)}`); + + const exec = new ModularActionsBuilder(); + const routerBal = exec.staticCall(usdc, encodeBalanceOf(routerAddress)); + + exec + .call(usdc, encodeTransfer(signerAddress, 0n)) + .spliceArg(1, routerBal.ref().returnWord(0)); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const calldata = routerIface.encodeFunctionData('performModularExecution', [ + ZERO_BYTES32, + exec.toActions(), + ]); + + console.log( + 'Sending performModularExecution (balanceOf → transfer with spliced amount)...', + ); + const tx = await signer.sendTransaction({ + to: routerAddress, + data: calldata, + }); + console.log(`Tx hash: ${tx.hash}`); + const receipt = await tx.wait(); + + if (receipt == null || receipt.status !== 1) { + throw new Error('Transaction failed or missing receipt'); + } + + logTxnSummary( + 'Polygon — withdraw router USDC to caller via performModularExecution', + chainId, + receipt, + ); + + const { balance: signerAfter } = await getWalletErc20Balance( + usdc, + signerAddress, + provider, + ); + console.log(`Signer USDC after: ${ethers.formatUnits(signerAfter, 6)}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts new file mode 100644 index 0000000..cc4ccb4 --- /dev/null +++ b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts @@ -0,0 +1,232 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * returnData mode is not available for KyberSwap — it routes output directly to recipient. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | balance-of (0x02) +const FLAGS = 0x03n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Sets sender and recipient both to the router so output lands in the router + * for balanceOf delta measurement and post-fee deduction. + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(inputAmount, ROUTER_POLYGON); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: ksRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.postFee.returndata.ts b/scripts/e2e/swap/kyberswap.postFee.returndata.ts new file mode 100644 index 0000000..06e35dd --- /dev/null +++ b/scripts/e2e/swap/kyberswap.postFee.returndata.ts @@ -0,0 +1,227 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Build uses sender = recipient = router so gross USDC stays on the router until post-fee forward + * (same shape as the balanceOf post-fee script; only the output-measurement flag differs). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | returndata (bit1=0 ⇒ no 0x02) +const FLAGS = 0x01n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Post-fee path: both sender and recipient are the router so output settles on-contract before fee. + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(inputAmount, ROUTER_POLYGON); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: ksRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts new file mode 100644 index 0000000..1043d72 --- /dev/null +++ b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts @@ -0,0 +1,238 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * KyberSwap build calldata encodes exact input amounts, so the quote is for swapInput + * (inputAmount − preFeeAmount) to match the router's approval amount at execution time. + * returnData mode is not available for KyberSwap — it routes output directly to recipient. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | balance-of (0x02) +const FLAGS = 0x02n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Sets sender and recipient both to the router so output lands in the router + * for balanceOf delta measurement. + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + // quote for the reduced swap input so KyberSwap calldata encodes the correct amount + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(swapInput, ROUTER_POLYGON); + + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: ksRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.preFee.returndata.ts b/scripts/e2e/swap/kyberswap.preFee.returndata.ts new file mode 100644 index 0000000..95df823 --- /dev/null +++ b/scripts/e2e/swap/kyberswap.preFee.returndata.ts @@ -0,0 +1,234 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Pre-fee + returndata: swap output must go to `receiver` (signer); the router decodes amount from + * returndata. Quote uses swapInput = inputAmount − fee so calldata matches the post-fee swap size. + * + * Kyber build: sender = router (executor), recipient = user (net output destination). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | returndata (bit1=0 ⇒ no 0x02) +const FLAGS = 0x00n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Pre-fee returndata: router executes; tokens are sent to `outputRecipient` (user). + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, + outputRecipient: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: outputRecipient, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + // quote for the reduced swap input so KyberSwap calldata encodes the correct amount + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(swapInput, ROUTER_POLYGON, signerAddress); + + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: ksRouter, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.postFee.balanceOf.ts b/scripts/e2e/swap/swap.postFee.balanceOf.ts index 50e518c..7faf26d 100644 --- a/scripts/e2e/swap/swap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.postFee.balanceOf.ts @@ -132,6 +132,7 @@ async function main() { inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, + signerAddress, FLAGS, { receiver: signerAddress, amount: feeAmount }, { diff --git a/scripts/e2e/swap/swap.postFee.returndata.ts b/scripts/e2e/swap/swap.postFee.returndata.ts index 7280e6f..01eb4b7 100644 --- a/scripts/e2e/swap/swap.postFee.returndata.ts +++ b/scripts/e2e/swap/swap.postFee.returndata.ts @@ -127,6 +127,7 @@ async function main() { inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, + signerAddress, FLAGS, { receiver: signerAddress, amount: feeAmount }, { diff --git a/scripts/e2e/swap/swap.preFee.balanceOf.ts b/scripts/e2e/swap/swap.preFee.balanceOf.ts index 7bac539..d21c98b 100644 --- a/scripts/e2e/swap/swap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.preFee.balanceOf.ts @@ -132,6 +132,7 @@ async function main() { inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, + signerAddress, FLAGS, { receiver: signerAddress, amount: feeAmount }, { diff --git a/scripts/e2e/swap/swap.preFee.returndata.ts b/scripts/e2e/swap/swap.preFee.returndata.ts index 61c7d76..7de1155 100644 --- a/scripts/e2e/swap/swap.preFee.returndata.ts +++ b/scripts/e2e/swap/swap.preFee.returndata.ts @@ -127,6 +127,7 @@ async function main() { inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, + signerAddress, FLAGS, { receiver: signerAddress, amount: feeAmount }, { diff --git a/scripts/e2e/swap/zerox.postFee.balanceOf.ts b/scripts/e2e/swap/zerox.postFee.balanceOf.ts new file mode 100644 index 0000000..593ef82 --- /dev/null +++ b/scripts/e2e/swap/zerox.postFee.balanceOf.ts @@ -0,0 +1,228 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * 0x v2 uses the AllowanceHolder contract (0x000…1fF3) as both the approval target and the swap + * call target. taker=router (router holds the tokens and makes the AH call), recipient=router + * (output lands in router for post-fee deduction and balanceOf delta measurement). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | balance-of (0x02) +const FLAGS = 0x03n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker and recipient are both set to the router so the router executes the call + * and receives the output for post-fee deduction and balanceOf delta measurement. + * + * Balance/allowance issues in the response are expected at quote time (the router + * will have the tokens at execution time) and are ignored. + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote(inputAmount, ROUTER_POLYGON, ROUTER_POLYGON, signerAddress); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(buyAmount, FEE_BPS); + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + // The 0x AllowanceHolder is the approval spender; swapTarget should equal ALLOWANCE_HOLDER + const approvalSpender = ALLOWANCE_HOLDER; + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.postFee.returndata.ts b/scripts/e2e/swap/zerox.postFee.returndata.ts new file mode 100644 index 0000000..3f88890 --- /dev/null +++ b/scripts/e2e/swap/zerox.postFee.returndata.ts @@ -0,0 +1,217 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * 0x: taker=router, recipient=router so gross USDC stays on the router for post-fee settle + * (same quote shape as balanceOf post-fee; only the output-measurement flag differs). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// post-fee (0x01) | returndata (no 0x02) +const FLAGS = 0x01n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker and recipient are the router so execution and settlement stay on-contract for post-fee. + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote(inputAmount, ROUTER_POLYGON, ROUTER_POLYGON, signerAddress); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(buyAmount, FEE_BPS); + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + const approvalSpender = ALLOWANCE_HOLDER; + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.preFee.balanceOf.ts b/scripts/e2e/swap/zerox.preFee.balanceOf.ts new file mode 100644 index 0000000..ca37956 --- /dev/null +++ b/scripts/e2e/swap/zerox.preFee.balanceOf.ts @@ -0,0 +1,235 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * 0x v2 uses the AllowanceHolder contract (0x000…1fF3) as both the approval target and the swap + * call target. taker=router (router holds the tokens and makes the AH call), recipient=router + * (output lands in router for balanceOf delta measurement). + * + * The quote is for swapInput (inputAmount − preFeeAmount) so the 0x calldata matches the + * router's approval amount at execution time. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | balance-of (0x02) +const FLAGS = 0x02n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker and recipient are both set to the router so the router executes the call + * and receives the output for balanceOf delta measurement. + * + * Balance/allowance issues in the response are expected at quote time (the router + * will have the tokens at execution time) and are ignored. + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + // quote for the reduced swap input so 0x calldata encodes the correct sell amount + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote(swapInput, ROUTER_POLYGON, ROUTER_POLYGON, signerAddress); + + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + // The 0x AllowanceHolder is the approval spender; swapTarget should equal ALLOWANCE_HOLDER + const approvalSpender = ALLOWANCE_HOLDER; + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.preFee.returndata.ts b/scripts/e2e/swap/zerox.preFee.returndata.ts new file mode 100644 index 0000000..2ca1b56 --- /dev/null +++ b/scripts/e2e/swap/zerox.preFee.returndata.ts @@ -0,0 +1,227 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * 0x: taker=router (AllowanceHolder entry), recipient=signer so output USDC goes to the user + * while the router decodes `filledAmount` / return data per `returnDataWordOffset`. + * + * Quote uses swapInput (inputAmount − preFeeAmount) so calldata matches execution. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; + +// pre-fee (0x00) | returndata (no 0x02) +const FLAGS = 0x00n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker=router, recipient=user so bought USDC is delivered to the user (pre-fee + returndata). + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote( + swapInput, + ROUTER_POLYGON, + signerAddress, + signerAddress, + ); + + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + const approvalSpender = ALLOWANCE_HOLDER; + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + ); + + const callData = routerIface.encodeFunctionData("swap", [ + ZERO_BYTES32, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + signerAddress, + FLAGS, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + ]); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index 206584a..2caa0e0 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -28,6 +28,7 @@ export const ROUTER_ABI = [ `function swap( bytes32 requestHash, (address user, address inputToken, uint256 inputAmount) input, + address receiver, uint256 flags, (address receiver, uint256 amount) fee, (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, From 1313cc4750931db5e05a9e77cfe887c611ead80b Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 18 May 2026 20:24:03 +0530 Subject: [PATCH 43/69] feat: rescueFunds --- src/combined/BungeeOpenRouterV2Unchecked.sol | 12 ++++++++ src/common/lib/RescueFundsLib.sol | 32 ++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/common/lib/RescueFundsLib.sol diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 6445c6b..5d55f84 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -8,6 +8,7 @@ import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; +import {RescueFundsLib} from "../common/lib/RescueFundsLib.sol"; /// @title BungeeOpenRouterV2Unchecked /// @notice Identical execution logic to `BungeeOpenRouterV2` with all backend @@ -762,4 +763,15 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { word := mload(add(add(ret, 0x20), offset)) } } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyOwner { + RescueFundsLib.rescueFunds(token_, rescueTo_, amount_); + } } + diff --git a/src/common/lib/RescueFundsLib.sol b/src/common/lib/RescueFundsLib.sol new file mode 100644 index 0000000..a18b950 --- /dev/null +++ b/src/common/lib/RescueFundsLib.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity =0.8.25; + +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; + +error ZeroAddress(); + +/// @title RescueFundsLib +/// @notice Pull tokens or native ETH from the calling contract to a recipient. +library RescueFundsLib { + address public constant ETH_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + error InvalidTokenAddress(); + + /// @param token_ ERC20 token or `ETH_ADDRESS` for native balance. + /// @param rescueTo_ Recipient; must not be zero. + /// @param amount_ Amount to transfer out of `address(this)`. + function rescueFunds(address token_, address rescueTo_, uint256 amount_) internal { + if (rescueTo_ == address(0)) { + revert ZeroAddress(); + } + + if (token_ == ETH_ADDRESS) { + SafeTransferLib.safeTransferETH(rescueTo_, amount_); + } else { + if (token_.code.length == 0) { + revert InvalidTokenAddress(); + } + SafeTransferLib.safeTransfer(token_, rescueTo_, amount_); + } + } +} From c58077b0804104796d4e9247431759999e8d16cd Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 12:56:15 +0530 Subject: [PATCH 44/69] refactor: remove monolithic exec code --- src/combined/BungeeOpenRouterV2Unchecked.sol | 126 +------------------ 1 file changed, 2 insertions(+), 124 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 5d55f84..d0d4cce 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -33,10 +33,6 @@ import {RescueFundsLib} from "../common/lib/RescueFundsLib.sol"; contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { using SafeTransferLib for address; - // ========================================================================= - // Monolithic execution types - // ========================================================================= - struct InputData { address user; address inputToken; @@ -63,18 +59,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { uint256 value; } - struct MonolithicExecution { - InputData input; - FeeData preFee; - SwapData swap; - FeeData postFee; - BridgeData bridge; - /// Packed flags; monolithic pipeline tests `BALANCE_FLAG_BIT_MASK` in `_execSwap`, - /// `BRIDGE_VALUE_FLAG_BIT_MASK` and `BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK` in `_doBridge`. - /// Fee timing uses `preFee` / `postFee` structs — `FEE_FLAG_BIT_MASK` (bit 0) is ignored here. - uint256 flags; - } - // ========================================================================= // Modular execution types // ========================================================================= @@ -92,7 +76,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // ========================================================================= - // Flags (swap / swapAndBridge / monolithic swap step) + // Flags (swap / swapAndBridge) // ========================================================================= // // Instead of bool parameters, one uint256 packs independent switches without adding @@ -133,7 +117,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Cleared — no runtime amount splice. // Set — splice finalAmount at uint16(flags >> BRIDGE_AMOUNT_POSITION_SHIFT). // - // Monolithic `performExecution` ignores `FEE_FLAG_BIT_MASK`; fee timing is `preFee`/`postFee` structs. /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. uint256 internal constant FEE_FLAG_BIT_MASK = 0x01; @@ -182,32 +165,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { receive() external payable {} - // ========================================================================= - // External: monolithic path - // ========================================================================= - - /** - * @notice Executes the monolithic pipeline without signature verification: - * pull via AH, optional pre-swap fee, optional swap, optional - * post-swap fee, bridge call with optional single-position amount splicing. - * @dev The caller MUST route through `AllowanceHolder.exec` so that - * `_msgSender()` resolves to `exec.input.user`. There is no nonce or - * deadline; replay protection is the caller's responsibility. - * Bit 0 (`FEE_FLAG_BIT_MASK`) is unused in monolithic runs; fee placement is `preFee` / `postFee` structs. - * `exec.flags` contributes `BALANCE_FLAG_BIT_MASK` to `_execSwap` and - * `BRIDGE_VALUE_FLAG_BIT_MASK` to bridge msg.value selection. - * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. - */ - function performExecution( - bytes32 requestHash, - MonolithicExecution calldata exec, - bytes calldata swapCallData, - bytes calldata bridgeCallData - ) external payable { - _runMonolithic(exec, swapCallData, bridgeCallData); - emit RequestExecuted(requestHash); - } - // ========================================================================= // External: standalone swap // ========================================================================= @@ -452,85 +409,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { emit RequestExecuted(requestHash); } - // ========================================================================= - // Internal: monolithic pipeline - // ========================================================================= - - function _runMonolithic( - MonolithicExecution calldata exec, - bytes calldata swapCallData, - bytes calldata bridgeCallData - ) internal { - if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { - revert InvalidExecution(); - } - - // 1. pull funds from user via AllowanceHolder - _pullFromUser(exec.input.inputToken, exec.input.user, exec.input.inputAmount); - - // 2. optional pre-swap fee in input token - if (exec.preFee.amount != 0) { - CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); - } - - // 3. optional swap, accounted via decoded returndata - address finalToken; - uint256 finalAmount; - if (exec.swap.target != address(0)) { - (finalToken, finalAmount) = _performSwap(exec, swapCallData); - } else { - if (exec.preFee.amount > exec.input.inputAmount) { - revert InsufficientFunds(); - } - finalToken = exec.input.inputToken; - unchecked { - finalAmount = exec.input.inputAmount - exec.preFee.amount; - } - } - - // 4. optional post-swap fee in final token - if (exec.postFee.amount != 0) { - if (exec.postFee.amount > finalAmount) { - revert InsufficientFunds(); - } - CurrencyLib.transfer(finalToken, exec.postFee.receiver, exec.postFee.amount); - unchecked { - finalAmount -= exec.postFee.amount; - } - } - - // 5. bridge: splice, approve, call - _finishMonolithicBridge(exec, finalToken, finalAmount, bridgeCallData); - } - - function _finishMonolithicBridge( - MonolithicExecution calldata exec, - address finalToken, - uint256 finalAmount, - bytes calldata bridgeCallData - ) internal { - _doBridge(finalToken, finalAmount, exec.bridge, bridgeCallData, exec.flags); - } - - function _performSwap(MonolithicExecution calldata exec, bytes calldata swapCallData) - internal - returns (address finalToken, uint256 finalAmount) - { - if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - uint256 swapInput; - unchecked { - swapInput = exec.input.inputAmount - exec.preFee.amount; - } - SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); - } - - // Monolithic path: only `BALANCE_FLAG_BIT_MASK` is read for `_execSwap`; fee uses `preFee` / `postFee`, not bit 0. - // Swap output always lands at this contract; it feeds directly into the bridge step. - finalAmount = _execSwap(exec.swap, swapCallData, exec.flags & BALANCE_FLAG_BIT_MASK != 0, address(this)); - if (finalAmount < exec.swap.minOutput) revert SwapOutputInsufficient(); - finalToken = exec.swap.outputToken; - } - // ========================================================================= // Internal: swap / fee / bridge helpers // ========================================================================= @@ -705,7 +583,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } // ========================================================================= - // Internal: simple call dispatcher (used by monolithic path) + // Internal: simple call dispatcher // ========================================================================= function _doCall(address target, uint256 value, bytes memory data) internal { From 85a7d2b01ec1251e742d2f985411b073e8526e19 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 15:35:16 +0530 Subject: [PATCH 45/69] feat: slither --- package.json | 3 ++- scripts/docker-slither.sh | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 scripts/docker-slither.sh diff --git a/package.json b/package.json index feeaa09..4756cca 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "compile": "hardhat compile", "deploy:v2": "hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network", - "typechain": "hardhat typechain" + "typechain": "hardhat typechain", + "slither": "bash scripts/docker-slither.sh" }, "devDependencies": { "@arbitrum/sdk": "^4.0.5", diff --git a/scripts/docker-slither.sh b/scripts/docker-slither.sh new file mode 100644 index 0000000..b9e25d5 --- /dev/null +++ b/scripts/docker-slither.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Run Slither inside trailofbits/eth-security-toolbox with Foundry compilation. +# Uses forge in the container instead of solc-select (avoids solc-select 403s on binary list fetch). +# Remappings are read from remappings.txt so npm does not need a multiline tr(1) in package.json. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if [[ ! -f remappings.txt ]]; then + echo "docker-slither.sh: remappings.txt not found in ${ROOT}" >&2 + exit 1 +fi + +REMAPS="" +while IFS= read -r line || [[ -n "${line}" ]]; do + if [[ -n "${line}" ]]; then + if [[ -n "${REMAPS}" ]]; then + REMAPS+=" " + fi + REMAPS+="${line}" + fi +done < remappings.txt + +SLITHER_ARGS=("$@") +if [[ ${#SLITHER_ARGS[@]} -eq 0 ]]; then + SLITHER_ARGS=(.) +fi +if [[ ${#SLITHER_ARGS[@]} -eq 1 && "${SLITHER_ARGS[0]}" == *.sol ]]; then + sol_file="${SLITHER_ARGS[0]}" + base="$(basename "${sol_file}")" + # --include-paths takes a regex; escape dots so ".sol" is literal. + include_regex="${base//./\\.}" + SLITHER_ARGS=(. --include-paths "${include_regex}") +fi + +DOCKER_FLAGS=( + -t + --rm + -v "${ROOT}:/poc-openrouter" + -w /poc-openrouter + --platform linux/amd64 + --entrypoint slither +) + +# Do not mount ~/.foundry: host macOS forge/solc binaries break Linux exec (126 / Exec format error). + +exec docker run "${DOCKER_FLAGS[@]}" trailofbits/eth-security-toolbox "${SLITHER_ARGS[@]}" \ + --compile-force-framework forge \ + --solc-remaps "${REMAPS}" \ + --solc-args '--allow-paths /' From 9775932ae1924969d14402530fde8e4f8acd739e Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 19 May 2026 14:40:56 +0400 Subject: [PATCH 46/69] fix: fork tests --- foundry.toml | 5 +++ src/combined/BungeeOpenRouterV2Unchecked.sol | 1 - test/poc/OneInchCctpOpenRouterPoC.t.sol | 39 ++++++++----------- test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 2 +- ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 3 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/foundry.toml b/foundry.toml index 34b3732..b73e29d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,10 +7,15 @@ evm_version = "cancun" optimizer = true optimizer_runs = 2_000 via_ir = false +no_match_path = "test/poc/**" remappings = [ "solady/=lib/solady/", "forge-std/=lib/forge-std/src/", ] +[profile.poc] +match_path = "test/poc/*.t.sol" +no_match_path = "NO_MATCHING_TEST_PATH" + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index d0d4cce..370139d 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -652,4 +652,3 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { RescueFundsLib.rescueFunds(token_, rescueTo_, amount_); } } - diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 6d5ee09..7a31481 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -84,7 +84,7 @@ contract OneInchCctpOpenRouterPoCTest is Test { POLYGON_AAVE, inputAmount, payable(address(router)), - abi.encodeCall(router.performModularExecution, (actions)) + abi.encodeCall(router.performModularExecution, (keccak256("one-inch-cctp-modular"), actions)) ); uint256 executeGasUsed = gasBeforeExecute - gasleft(); emit log_named_uint("AllowanceHolder.exec -> router.performModularExecution gas used", executeGasUsed); @@ -119,20 +119,15 @@ contract OneInchCctpOpenRouterPoCTest is Test { uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); - (Router.MonolithicExecution memory exec, bytes memory swapCallData, bytes memory bridgeCallData) = - _buildMonolithicExecution(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); + bytes memory routerCallData = _swapAndBridgeCallData(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); vm.prank(FIXTURE_RECIPIENT); uint256 gasBeforeExecute = gasleft(); IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( - address(router), - POLYGON_AAVE, - inputAmount, - payable(address(router)), - abi.encodeCall(router.performExecution, (exec, swapCallData, bridgeCallData)) + address(router), POLYGON_AAVE, inputAmount, payable(address(router)), routerCallData ); uint256 executeGasUsed = gasBeforeExecute - gasleft(); - emit log_named_uint("AllowanceHolder.exec -> router.performExecution gas used", executeGasUsed); + emit log_named_uint("AllowanceHolder.exec -> router.swapAndBridge gas used", executeGasUsed); _assertMonolithicPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); } @@ -236,18 +231,18 @@ contract OneInchCctpOpenRouterPoCTest is Test { ); } - function _buildMonolithicExecution(uint256 inputAmount, bytes memory swapCalldata) + function _swapAndBridgeCallData(uint256 inputAmount, bytes memory swapCalldata) internal pure - returns (Router.MonolithicExecution memory exec, bytes memory swapCallData, bytes memory bridgeCallData) + returns (bytes memory) { - swapCallData = swapCalldata; - bridgeCallData = _emptyDepositForBurnCalldata(); - - exec = Router.MonolithicExecution({ - input: Router.InputData({user: FIXTURE_RECIPIENT, inputToken: POLYGON_AAVE, inputAmount: inputAmount}), - preFee: Router.FeeData({receiver: address(0), amount: 0}), - swap: Router.SwapData({ + return abi.encodeWithSelector( + Router.swapAndBridge.selector, + keccak256("one-inch-cctp-swap-and-bridge"), + Router.InputData({user: FIXTURE_RECIPIENT, inputToken: POLYGON_AAVE, inputAmount: inputAmount}), + uint256(0x01 | 0x08 | (uint256(4) << 16)), + Router.FeeData({receiver: FEE_RECIPIENT, amount: ROUTE_FEE_USDC}), + Router.SwapData({ target: ONEINCH_SWAP_TARGET, approvalSpender: ONEINCH_SWAP_TARGET, outputToken: POLYGON_USDC, @@ -255,10 +250,10 @@ contract OneInchCctpOpenRouterPoCTest is Test { minOutput: EXPECTED_SWAP_OUTPUT_USDC, returnDataWordOffset: 0 }), - postFee: Router.FeeData({receiver: FEE_RECIPIENT, amount: ROUTE_FEE_USDC}), - bridge: Router.BridgeData({target: CCTP_TOKEN_MESSENGER_V2, approvalSpender: CCTP_TOKEN_MESSENGER_V2, value: 0}), - flags: 0x08 | (uint256(4) << 16) - }); + swapCalldata, + Router.BridgeData({target: CCTP_TOKEN_MESSENGER_V2, approvalSpender: CCTP_TOKEN_MESSENGER_V2, value: 0}), + _emptyDepositForBurnCalldata() + ); } function _assertPocResult( diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 537f5fa..34c9db1 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -67,7 +67,7 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { uint256 spokePoolWethBefore = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL); uint256 gasBeforeExecute = gasleft(); - bytes[] memory results = router.performModularExecution(actions); + bytes[] memory results = router.performModularExecution(keccak256("open-ocean-across-modular"), actions); uint256 executeGasUsed = gasBeforeExecute - gasleft(); emit log_named_uint("router.performModularExecution gas used", executeGasUsed); diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index b653b75..272a76e 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -101,7 +101,8 @@ contract OpenOceanStargateNativeOpenRouterPoCTest is Test { ); uint256 gasBeforeExecute = gasleft(); - bytes[] memory results = router.performModularExecution(actions); + bytes[] memory results = + router.performModularExecution(keccak256("open-ocean-stargate-native-modular"), actions); uint256 executeGasUsed = gasBeforeExecute - gasleft(); emit log_named_uint("router.performModularExecution gas used", executeGasUsed); From 3da9283dbcc36449ac352f1a898739ee5bd3f91a Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 19 May 2026 14:47:59 +0400 Subject: [PATCH 47/69] feat: mock tests --- .../BungeeOpenRouterV2UncheckedBridge.t.sol | 193 +++++++++ .../BungeeOpenRouterV2UncheckedSwap.t.sol | 303 ++++++++++++++ ...eeOpenRouterV2UncheckedSwapAndBridge.t.sol | 162 ++++++++ .../BungeeOpenRouterV2UncheckedTestBase.sol | 376 ++++++++++++++++++ 4 files changed, 1034 insertions(+) create mode 100644 test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol create mode 100644 test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol create mode 100644 test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol create mode 100644 test/combined/BungeeOpenRouterV2UncheckedTestBase.sol diff --git a/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol b/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol new file mode 100644 index 0000000..2e9010e --- /dev/null +++ b/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; +import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; + +contract BungeeOpenRouterV2UncheckedBridgeTest is BungeeOpenRouterV2UncheckedTestBase { + function test_bridge_erc20() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: 0 + }), + "input token initial" + ); + + _execBridge( + address(inputToken), + INPUT_AMOUNT, + 0, + Router.FeeData({receiver: address(0), amount: 0}), + _bridgeData(address(inputToken), 0), + _bridgeCallData(address(inputToken), INPUT_AMOUNT) + ); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: INPUT_AMOUNT, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: 0 + }), + "input token final" + ); + assertEq(bridgeTarget.receivedToken(), address(inputToken)); + assertEq(bridgeTarget.receivedAmount(), INPUT_AMOUNT); + } + + function test_bridge_native() public { + vm.deal(USER, INPUT_AMOUNT); + uint256 testContractBalance = address(this).balance; + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native initial" + ); + + _execBridge( + NATIVE_TOKEN, + INPUT_AMOUNT, + INPUT_AMOUNT, + Router.FeeData({receiver: address(0), amount: 0}), + _bridgeData(NATIVE_TOKEN, INPUT_AMOUNT), + _bridgeCallData(NATIVE_TOKEN, INPUT_AMOUNT) + ); + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: INPUT_AMOUNT, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native final" + ); + assertEq(bridgeTarget.receivedToken(), NATIVE_TOKEN); + assertEq(bridgeTarget.receivedAmount(), INPUT_AMOUNT); + } + + function test_bridge_withErc20Fee() public { + uint256 bridgeAmount = INPUT_AMOUNT - FEE_AMOUNT; + _deal(address(inputToken), USER, INPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: 0 + }), + "input token initial" + ); + + _execBridge( + address(inputToken), + INPUT_AMOUNT, + 0, + Router.FeeData({receiver: FEE_RECIPIENT, amount: FEE_AMOUNT}), + _bridgeData(address(inputToken), 0), + _bridgeCallData(address(inputToken), bridgeAmount) + ); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: bridgeAmount, + receiver: 0, + feeRecipient: FEE_AMOUNT, + allowanceHolder: 0, + testContract: 0 + }), + "input token final" + ); + assertEq(bridgeTarget.receivedToken(), address(inputToken)); + assertEq(bridgeTarget.receivedAmount(), bridgeAmount); + } + + function test_bridge_withNativeFee() public { + uint256 bridgeAmount = INPUT_AMOUNT - FEE_AMOUNT; + vm.deal(USER, INPUT_AMOUNT); + uint256 testContractBalance = address(this).balance; + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native initial" + ); + + _execBridge( + NATIVE_TOKEN, + INPUT_AMOUNT, + INPUT_AMOUNT, + Router.FeeData({receiver: FEE_RECIPIENT, amount: FEE_AMOUNT}), + _bridgeData(NATIVE_TOKEN, bridgeAmount), + _bridgeCallData(NATIVE_TOKEN, bridgeAmount) + ); + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: bridgeAmount, + receiver: 0, + feeRecipient: FEE_AMOUNT, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native final" + ); + assertEq(bridgeTarget.receivedToken(), NATIVE_TOKEN); + assertEq(bridgeTarget.receivedAmount(), bridgeAmount); + } +} diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol b/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol new file mode 100644 index 0000000..38a3244 --- /dev/null +++ b/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; +import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; + +contract BungeeOpenRouterV2UncheckedSwapTest is BungeeOpenRouterV2UncheckedTestBase { + function test_swapWithReturnData() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + _assertNativeBalances(0, 0, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: 0, + fee: _feeData(0), + swapData: _swapData(address(inputToken), address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + address(inputToken), address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + _assertNativeBalances(0, 0, 0, 0, "after native"); + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_swapWithoutReturnDataUsesBalanceDelta() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + _assertNativeBalances(0, 0, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: BALANCE_FLAG_BIT_MASK, + fee: _feeData(0), + swapData: _swapData(address(inputToken), address(outputToken), SWAP_OUTPUT_AMOUNT, false), + swapCallData: _swapNoReturnCallData( + address(inputToken), address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + _assertNativeBalances(0, 0, 0, 0, "after native"); + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_swapERC20ToNative() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(NATIVE_TOKEN, address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertNativeBalances(0, SWAP_OUTPUT_AMOUNT, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: 0, + fee: _feeData(0), + swapData: _swapData(address(inputToken), NATIVE_TOKEN, SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData(address(inputToken), NATIVE_TOKEN, INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertNativeBalances(0, 0, SWAP_OUTPUT_AMOUNT, 0, "after native"); + + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_swapNativeToERC20() public { + _deal(NATIVE_TOKEN, USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + + _assertNativeBalances(INPUT_AMOUNT, 0, 0, 0, "before native"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: NATIVE_TOKEN, + inputAmount: INPUT_AMOUNT, + value: INPUT_AMOUNT, + receiver: RECEIVER, + flags: 0, + fee: _feeData(0), + swapData: _swapData(NATIVE_TOKEN, address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData(NATIVE_TOKEN, address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertNativeBalances(0, INPUT_AMOUNT, 0, 0, "after native"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + + _assertSwapInput(NATIVE_TOKEN, INPUT_AMOUNT); + } + + function test_swapERC20ToERC20() public { + test_swapWithReturnData(); + } + + function test_prefeeSwapWithNativeFee() public { + uint256 swapInput = INPUT_AMOUNT - FEE_AMOUNT; + + _deal(NATIVE_TOKEN, USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + + _assertNativeBalances(INPUT_AMOUNT, 0, 0, 0, "before native"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: NATIVE_TOKEN, + inputAmount: INPUT_AMOUNT, + value: INPUT_AMOUNT, + receiver: RECEIVER, + flags: 0, + fee: _feeData(FEE_AMOUNT), + swapData: _swapDataWithValue(NATIVE_TOKEN, address(outputToken), SWAP_OUTPUT_AMOUNT, swapInput), + swapCallData: _swapCallData(NATIVE_TOKEN, address(outputToken), swapInput, SWAP_OUTPUT_AMOUNT, RECEIVER) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertNativeBalances(0, swapInput, 0, FEE_AMOUNT, "after native"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + + _assertSwapInput(NATIVE_TOKEN, swapInput); + } + + function test_prefeeSwapWithERC20Fee() public { + uint256 swapInput = INPUT_AMOUNT - FEE_AMOUNT; + + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + _assertNativeBalances(0, 0, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: 0, + fee: _feeData(FEE_AMOUNT), + swapData: _swapData(address(inputToken), address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + address(inputToken), address(outputToken), swapInput, SWAP_OUTPUT_AMOUNT, RECEIVER + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, swapInput, 0, FEE_AMOUNT, "after input token"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + _assertNativeBalances(0, 0, 0, 0, "after native"); + _assertSwapInput(address(inputToken), swapInput); + } + + function test_postfeeSwapWithNativeFee() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(NATIVE_TOKEN, address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertNativeBalances(0, SWAP_OUTPUT_AMOUNT, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: FEE_FLAG_BIT_MASK, + fee: _feeData(FEE_AMOUNT), + swapData: _swapData(address(inputToken), NATIVE_TOKEN, SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + address(inputToken), NATIVE_TOKEN, INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, address(router) + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertNativeBalances(0, 0, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, FEE_AMOUNT, "after native"); + + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_postfeeSwapWithERC20Fee() public { + _deal(NATIVE_TOKEN, USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + + _assertNativeBalances(INPUT_AMOUNT, 0, 0, 0, "before native"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: NATIVE_TOKEN, + inputAmount: INPUT_AMOUNT, + value: INPUT_AMOUNT, + receiver: RECEIVER, + flags: FEE_FLAG_BIT_MASK, + fee: _feeData(FEE_AMOUNT), + swapData: _swapData(NATIVE_TOKEN, address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + NATIVE_TOKEN, address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, address(router) + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, "final amount"); + + _assertNativeBalances(0, INPUT_AMOUNT, 0, 0, "after native"); + _assertERC20Balances( + address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, FEE_AMOUNT, "after output token" + ); + + _assertSwapInput(NATIVE_TOKEN, INPUT_AMOUNT); + } + + function _feeData(uint256 amount) private pure returns (Router.FeeData memory) { + return Router.FeeData({receiver: FEE_RECIPIENT, amount: amount}); + } + + function _emptyNativeBalances() private view returns (Balances memory balances) { + balances.testContract = address(this).balance; + } + + function _assertERC20Balances( + address token, + uint256 user, + uint256 swapTargetBalance, + uint256 receiver, + uint256 feeRecipient, + string memory label + ) private view { + Balances memory balances = _emptyBalances(); + balances.user = user; + balances.swapTarget = swapTargetBalance; + balances.receiver = receiver; + balances.feeRecipient = feeRecipient; + _assertTokenBalances(token, balances, label); + } + + function _assertNativeBalances( + uint256 user, + uint256 swapTargetBalance, + uint256 receiver, + uint256 feeRecipient, + string memory label + ) private view { + Balances memory balances = _emptyNativeBalances(); + balances.user = user; + balances.swapTarget = swapTargetBalance; + balances.receiver = receiver; + balances.feeRecipient = feeRecipient; + _assertTokenBalances(NATIVE_TOKEN, balances, label); + } + + function _assertSwapInput(address input, uint256 amount) private view { + assertEq(swapTarget.storedInputToken(), input, "swap input token"); + assertEq(swapTarget.storedInputAmount(), amount, "swap input amount"); + } +} diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol b/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol new file mode 100644 index 0000000..b3c0efd --- /dev/null +++ b/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; +import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; + +contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2UncheckedTestBase { + enum FeeMode { + None, + Pre, + Post + } + + struct Scenario { + address input; + address output; + FeeMode feeMode; + uint256 swapInput; + uint256 bridgeAmount; + } + + function test_swapAndBridge_noFee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.None); + } + + function test_swapAndBridge_noFee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.None); + } + + function test_swapAndBridge_noFee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.None); + } + + function test_swapAndBridge_prefee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Pre); + } + + function test_swapAndBridge_prefee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Pre); + } + + function test_swapAndBridge_prefee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Pre); + } + + function test_swapAndBridge_postfee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Post); + } + + function test_swapAndBridge_postfee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Post); + } + + function test_swapAndBridge_postfee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Post); + } + + function _runSwapAndBridge(address input, address output, FeeMode feeMode) internal { + Scenario memory scenario = _scenario(input, output, feeMode); + + _fundSwapAndBridge(scenario.input, scenario.output); + if (scenario.input != NATIVE_TOKEN) _approveInputToken(INPUT_AMOUNT); + + _assertSwapAndBridgeInitial(scenario.input, scenario.output); + _executeSwapAndBridge(scenario); + _assertSwapAndBridgeFinal(scenario); + + assertEq(swapTarget.storedInputToken(), scenario.input); + assertEq(swapTarget.storedInputAmount(), scenario.swapInput); + assertEq(bridgeTarget.receivedToken(), scenario.output); + assertEq(bridgeTarget.receivedAmount(), scenario.bridgeAmount); + } + + function _scenario(address input, address output, FeeMode feeMode) + internal + pure + returns (Scenario memory scenario) + { + scenario.input = input; + scenario.output = output; + scenario.feeMode = feeMode; + scenario.swapInput = _swapInput(feeMode); + scenario.bridgeAmount = _bridgeAmount(feeMode); + } + + function _executeSwapAndBridge(Scenario memory scenario) internal { + _execThroughAllowanceHolder( + scenario.input, + INPUT_AMOUNT, + scenario.input == NATIVE_TOKEN ? INPUT_AMOUNT : 0, + _swapAndBridgeCallData(scenario) + ); + } + + function _swapAndBridgeCallData(Scenario memory scenario) internal view returns (bytes memory) { + return abi.encodeCall( + router.swapAndBridge, + ( + keccak256("swap-and-bridge"), + Router.InputData({user: USER, inputToken: scenario.input, inputAmount: INPUT_AMOUNT}), + _flags(scenario.output, scenario.feeMode), + _fee(scenario.feeMode), + _swapDataWithValue( + scenario.input, + scenario.output, + SWAP_OUTPUT_AMOUNT, + scenario.input == NATIVE_TOKEN ? scenario.swapInput : 0 + ), + _swapNoReturnCallData( + scenario.input, scenario.output, scenario.swapInput, SWAP_OUTPUT_AMOUNT, address(router) + ), + _bridgeData(scenario.output, 0), + _bridgeCallData(scenario.output, 0) + ) + ); + } + + function _fundSwapAndBridge(address input, address output) internal { + _deal(input, USER, INPUT_AMOUNT); + _deal(output, address(swapTarget), SWAP_OUTPUT_AMOUNT); + } + + function _assertSwapAndBridgeInitial(address input, address output) internal view { + Balances memory inputBalances = _emptyBalancesFor(input); + inputBalances.user = INPUT_AMOUNT; + _assertTokenBalances(input, inputBalances, "input initial"); + Balances memory outputBalances = _emptyBalancesFor(output); + outputBalances.swapTarget = SWAP_OUTPUT_AMOUNT; + _assertTokenBalances(output, outputBalances, "output initial"); + } + + function _assertSwapAndBridgeFinal(Scenario memory scenario) internal view { + Balances memory inputBalances = _emptyBalancesFor(scenario.input); + inputBalances.swapTarget = scenario.swapInput; + inputBalances.feeRecipient = scenario.feeMode == FeeMode.Pre ? FEE_AMOUNT : 0; + _assertTokenBalances(scenario.input, inputBalances, "input final"); + Balances memory outputBalances = _emptyBalancesFor(scenario.output); + outputBalances.bridgeTarget = scenario.bridgeAmount; + outputBalances.feeRecipient = scenario.feeMode == FeeMode.Post ? FEE_AMOUNT : 0; + _assertTokenBalances(scenario.output, outputBalances, "output final"); + } + + function _swapInput(FeeMode feeMode) internal pure returns (uint256) { + return feeMode == FeeMode.Pre ? INPUT_AMOUNT - FEE_AMOUNT : INPUT_AMOUNT; + } + + function _bridgeAmount(FeeMode feeMode) internal pure returns (uint256) { + return feeMode == FeeMode.Post ? SWAP_OUTPUT_AMOUNT - FEE_AMOUNT : SWAP_OUTPUT_AMOUNT; + } + + function _flags(address output, FeeMode feeMode) internal pure returns (uint256) { + uint256 flags = BALANCE_FLAG_BIT_MASK; + if (output == NATIVE_TOKEN) flags |= BRIDGE_VALUE_FLAG_BIT_MASK; + if (feeMode == FeeMode.Post) flags |= FEE_FLAG_BIT_MASK; + return _bridgeAmountSpliceFlags(flags); + } + + function _fee(FeeMode feeMode) internal pure returns (Router.FeeData memory) { + if (feeMode == FeeMode.None) return Router.FeeData({receiver: address(0), amount: 0}); + return Router.FeeData({receiver: FEE_RECIPIENT, amount: FEE_AMOUNT}); + } +} diff --git a/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol b/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol new file mode 100644 index 0000000..3b33b75 --- /dev/null +++ b/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {BungeeOpenRouterV2Unchecked as Router} from "../../src/combined/BungeeOpenRouterV2Unchecked.sol"; +import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; + +abstract contract BungeeOpenRouterV2UncheckedTestBase is Test { + uint256 internal constant FEE_FLAG_BIT_MASK = 0x01; + uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; + uint256 internal constant BRIDGE_VALUE_FLAG_BIT_MASK = 0x04; + uint256 internal constant BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK = 0x08; + uint256 internal constant BRIDGE_AMOUNT_POSITION_SHIFT = 16; + uint256 internal constant BRIDGE_AMOUNT_CALLDATA_OFFSET = 36; + + address internal constant NATIVE_TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + address internal constant USER = address(0xA11CE); + address internal constant RECEIVER = address(0xB0B); + address internal constant FEE_RECIPIENT = address(0xFEE); + + uint256 internal constant INPUT_AMOUNT = 100 ether; + uint256 internal constant SWAP_OUTPUT_AMOUNT = 175 ether; + uint256 internal constant FEE_AMOUNT = 7 ether; + + Router internal router; + MockERC20 internal inputToken; + MockERC20 internal outputToken; + MockSwap internal swapTarget; + MockBridge internal bridgeTarget; + + struct Balances { + uint256 user; + uint256 router; + uint256 swapTarget; + uint256 bridgeTarget; + uint256 receiver; + uint256 feeRecipient; + uint256 allowanceHolder; + uint256 testContract; + } + + struct SwapParams { + address input; + uint256 inputAmount; + uint256 value; + address receiver; + uint256 flags; + Router.FeeData fee; + Router.SwapData swapData; + bytes swapCallData; + } + + struct SwapAndBridgeParams { + address input; + uint256 inputAmount; + uint256 value; + uint256 flags; + Router.FeeData fee; + Router.SwapData swapData; + bytes swapCallData; + Router.BridgeData bridgeData; + bytes bridgeCallData; + } + + function setUp() public virtual { + vm.etch(address(ALLOWANCE_HOLDER), address(new MockAllowanceHolder()).code); + + router = new Router(address(this)); + inputToken = new MockERC20("Input Token", "IN"); + outputToken = new MockERC20("Output Token", "OUT"); + swapTarget = new MockSwap(); + bridgeTarget = new MockBridge(); + + vm.label(address(router), "router"); + vm.label(address(inputToken), "inputToken"); + vm.label(address(outputToken), "outputToken"); + vm.label(address(swapTarget), "swapTarget"); + vm.label(address(bridgeTarget), "bridgeTarget"); + vm.label(address(ALLOWANCE_HOLDER), "allowanceHolder"); + vm.label(USER, "user"); + vm.label(RECEIVER, "receiver"); + vm.label(FEE_RECIPIENT, "feeRecipient"); + } + + function _approveInputToken(uint256 amount) internal { + vm.prank(USER); + inputToken.approve(address(ALLOWANCE_HOLDER), amount); + } + + function _execThroughAllowanceHolder(address token, uint256 amount, uint256 value, bytes memory data) + internal + returns (bytes memory result) + { + vm.prank(USER); + result = IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec{value: value}( + address(router), token, amount, payable(address(router)), data + ); + } + + function _execSwap(SwapParams memory params) internal returns (uint256 finalAmount) { + bytes memory result = _execThroughAllowanceHolder( + params.input, + params.inputAmount, + params.value, + abi.encodeCall( + router.swap, + ( + keccak256("swap"), + Router.InputData({user: USER, inputToken: params.input, inputAmount: params.inputAmount}), + params.receiver, + params.flags, + params.fee, + params.swapData, + params.swapCallData + ) + ) + ); + finalAmount = abi.decode(result, (uint256)); + } + + function _execBridge( + address input, + uint256 inputAmount, + uint256 value, + Router.FeeData memory fee, + Router.BridgeData memory bridgeData, + bytes memory bridgeCallData + ) internal { + _execThroughAllowanceHolder( + input, + inputAmount, + value, + abi.encodeCall( + router.bridge, + ( + keccak256("bridge"), + Router.InputData({user: USER, inputToken: input, inputAmount: inputAmount}), + fee, + bridgeData, + bridgeCallData + ) + ) + ); + } + + function _execSwapAndBridge(SwapAndBridgeParams memory params) internal { + _execThroughAllowanceHolder( + params.input, + params.inputAmount, + params.value, + abi.encodeCall( + router.swapAndBridge, + ( + keccak256("swap-and-bridge"), + Router.InputData({user: USER, inputToken: params.input, inputAmount: params.inputAmount}), + params.flags, + params.fee, + params.swapData, + params.swapCallData, + params.bridgeData, + params.bridgeCallData + ) + ) + ); + } + + function _swapData(address input, address output, uint256 outputAmount, bool useReturnData) + internal + view + returns (Router.SwapData memory) + { + return Router.SwapData({ + target: address(swapTarget), + approvalSpender: input == NATIVE_TOKEN ? address(0) : address(swapTarget), + outputToken: output, + value: input == NATIVE_TOKEN ? INPUT_AMOUNT : 0, + minOutput: outputAmount, + returnDataWordOffset: useReturnData ? 0 : 0 + }); + } + + function _swapDataWithValue(address input, address output, uint256 outputAmount, uint256 value) + internal + view + returns (Router.SwapData memory) + { + return Router.SwapData({ + target: address(swapTarget), + approvalSpender: input == NATIVE_TOKEN ? address(0) : address(swapTarget), + outputToken: output, + value: value, + minOutput: outputAmount, + returnDataWordOffset: 0 + }); + } + + function _bridgeData(address token, uint256 value) internal view returns (Router.BridgeData memory) { + return Router.BridgeData({ + target: address(bridgeTarget), + approvalSpender: token == NATIVE_TOKEN ? address(0) : address(bridgeTarget), + value: value + }); + } + + function _swapCallData(address input, address output, uint256 inputAmount, uint256 outputAmount, address receiver) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(MockSwap.swap, (input, output, inputAmount, outputAmount, receiver)); + } + + function _swapNoReturnCallData( + address input, + address output, + uint256 inputAmount, + uint256 outputAmount, + address receiver + ) internal pure returns (bytes memory) { + return abi.encodeCall(MockSwap.swapNoReturn, (input, output, inputAmount, outputAmount, receiver)); + } + + function _bridgeCallData(address token, uint256 amount) internal pure returns (bytes memory) { + return abi.encodeCall(MockBridge.bridge, (token, amount)); + } + + function _bridgeAmountSpliceFlags(uint256 baseFlags) internal pure returns (uint256) { + return baseFlags | BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK + | (BRIDGE_AMOUNT_CALLDATA_OFFSET << BRIDGE_AMOUNT_POSITION_SHIFT); + } + + function _assertTokenBalances(address token, Balances memory expected, string memory label) internal view { + assertEq(_balanceOf(token, USER), expected.user, string.concat(label, ": user")); + assertEq(_balanceOf(token, address(router)), expected.router, string.concat(label, ": router")); + assertEq(_balanceOf(token, address(swapTarget)), expected.swapTarget, string.concat(label, ": swap")); + assertEq(_balanceOf(token, address(bridgeTarget)), expected.bridgeTarget, string.concat(label, ": bridge")); + assertEq(_balanceOf(token, RECEIVER), expected.receiver, string.concat(label, ": receiver")); + assertEq(_balanceOf(token, FEE_RECIPIENT), expected.feeRecipient, string.concat(label, ": fee recipient")); + assertEq( + _balanceOf(token, address(ALLOWANCE_HOLDER)), + expected.allowanceHolder, + string.concat(label, ": allowance holder") + ); + assertEq(_balanceOf(token, address(this)), expected.testContract, string.concat(label, ": test contract")); + } + + function _balanceOf(address token, address account) internal view returns (uint256) { + if (token == NATIVE_TOKEN) return account.balance; + return ERC20(token).balanceOf(account); + } + + function _emptyBalances() internal pure returns (Balances memory balances) {} + + function _emptyBalancesFor(address token) internal view returns (Balances memory balances) { + if (token == NATIVE_TOKEN) balances.testContract = address(this).balance; + } + + function _deal(address token, address account, uint256 amount) internal { + if (token == NATIVE_TOKEN) { + vm.deal(account, amount); + } else { + MockERC20(token).mint(account, amount); + } + } +} + +contract MockAllowanceHolder { + function exec(address, address, uint256, address payable target, bytes calldata data) + external + payable + returns (bytes memory result) + { + (bool success, bytes memory returndata) = target.call{value: msg.value}(bytes.concat(data, bytes20(msg.sender))); + if (!success) { + assembly ("memory-safe") { + revert(add(returndata, 0x20), mload(returndata)) + } + } + return returndata; + } + + function transferFrom(address token, address owner, address recipient, uint256 amount) external returns (bool) { + require(ERC20(token).transferFrom(owner, recipient, amount), "MockAllowanceHolder: transfer failed"); + return true; + } +} + +contract MockSwap { + address public storedInputToken; + uint256 public storedInputAmount; + + receive() external payable {} + + function swap(address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, address receiver) + external + payable + returns (uint256) + { + _swap(inputToken, outputToken, inputAmount, outputAmount, receiver); + return outputAmount; + } + + function swapNoReturn( + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount, + address receiver + ) external payable { + _swap(inputToken, outputToken, inputAmount, outputAmount, receiver); + } + + function _swap(address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, address receiver) + internal + { + storedInputToken = inputToken; + storedInputAmount += inputAmount; + + if (inputToken == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + require(msg.value == inputAmount, "MockSwap: bad native input"); + } else { + require(msg.value == 0, "MockSwap: unexpected value"); + require(ERC20(inputToken).transferFrom(msg.sender, address(this), inputAmount), "MockSwap: input failed"); + } + + if (outputToken == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + (bool success,) = receiver.call{value: outputAmount}(""); + require(success, "MockSwap: native output failed"); + } else { + require(ERC20(outputToken).transfer(receiver, outputAmount), "MockSwap: output failed"); + } + } +} + +contract MockBridge { + address public receivedToken; + uint256 public receivedAmount; + + receive() external payable {} + + function bridge(address token, uint256 amount) external payable { + receivedToken = token; + receivedAmount += amount; + + if (token == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + require(msg.value == amount, "MockBridge: bad native amount"); + } else { + require(msg.value == 0, "MockBridge: unexpected value"); + require(ERC20(token).transferFrom(msg.sender, address(this), amount), "MockBridge: transfer failed"); + } + } +} + +contract MockERC20 is ERC20 { + string private _name; + string private _symbol; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + function name() public view override returns (string memory) { + return _name; + } + + function symbol() public view override returns (string memory) { + return _symbol; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} From 783c88567170a935d88fbb4bebe01c8ef2b91ebf Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 19 May 2026 14:54:05 +0400 Subject: [PATCH 48/69] chore: format --- src/minimal/BungeeOpenRouterMinimalAH.sol | 9 +++++---- src/modular/BungeeOpenRouterModularAH.sol | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/minimal/BungeeOpenRouterMinimalAH.sol b/src/minimal/BungeeOpenRouterMinimalAH.sol index fbd0401..6f398a5 100644 --- a/src/minimal/BungeeOpenRouterMinimalAH.sol +++ b/src/minimal/BungeeOpenRouterMinimalAH.sol @@ -12,15 +12,16 @@ import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext contract BungeeOpenRouterMinimalAH is BungeeOpenRouterMinimal, AllowanceHolderContext { error CallerNotSignedUser(); - constructor(address _owner, address _openRouterSigner) - BungeeOpenRouterMinimal(_owner, _openRouterSigner) - {} + constructor(address _owner, address _openRouterSigner) BungeeOpenRouterMinimal(_owner, _openRouterSigner) {} /// @notice AllowanceHolder-aware entrypoint. Same role as /// `BungeeOpenRouterModularAH.performExecutionAH` - prevents a /// signed payload meant for user A from being submitted via user /// B's AllowanceHolder.exec to grief user A's nonce. - function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) external payable { + function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) + external + payable + { if (_msgSender() != signedUser) { revert CallerNotSignedUser(); } diff --git a/src/modular/BungeeOpenRouterModularAH.sol b/src/modular/BungeeOpenRouterModularAH.sol index e0f37cb..de860b2 100644 --- a/src/modular/BungeeOpenRouterModularAH.sol +++ b/src/modular/BungeeOpenRouterModularAH.sol @@ -27,14 +27,15 @@ import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext contract BungeeOpenRouterModularAH is BungeeOpenRouterModular, AllowanceHolderContext { error CallerNotSignedUser(); - constructor(address _owner, address _openRouterSigner) - BungeeOpenRouterModular(_owner, _openRouterSigner) - {} + constructor(address _owner, address _openRouterSigner) BungeeOpenRouterModular(_owner, _openRouterSigner) {} /// @notice AllowanceHolder-aware entrypoint. Bind the signed payload to a /// specific user so it can only be submitted via that user's /// `AllowanceHolder.exec` call. - function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) external payable { + function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) + external + payable + { if (_msgSender() != signedUser) { revert CallerNotSignedUser(); } From 01eb83b9b9b300841ae04c0d314ebb39419cf288 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 17:14:39 +0530 Subject: [PATCH 49/69] refactor: renames, refactors, reorders --- src/combined/BungeeOpenRouterV2Unchecked.sol | 474 ++++++++++--------- 1 file changed, 241 insertions(+), 233 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index d0d4cce..3237fc2 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -1,38 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; -import {Ownable} from "../common/utils/Ownable.sol"; +import {AccessControl} from "../common/utils/AccessControl.sol"; import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; import {RescueFundsLib} from "../common/lib/RescueFundsLib.sol"; - -/// @title BungeeOpenRouterV2Unchecked -/// @notice Identical execution logic to `BungeeOpenRouterV2` with all backend -/// signature verification removed. There are no nonce or deadline -/// fields; either entrypoint can be called by anyone. -/// -/// Fund safety still rests on AllowanceHolder's transient allowance -/// scoping (operator + owner + token): only the user whose address was -/// passed to `AllowanceHolder.exec` can authorise a pull of their own -/// funds. The `_msgSender() == user` check in `_pullFromUser` enforces -/// this at the contract level. -/// -/// Intended for development / testing environments where spinning up a -/// backend signer is inconvenient, or for operational flows where the -/// operator calls through AllowanceHolder directly without a separate -/// signing step. Do NOT deploy to production without adding an access -/// control layer appropriate to your threat model. -/// -/// @dev Both struct types mirror their `BungeeOpenRouterV2` counterparts but -/// drop the `nonce` and `deadline` fields, which are only relevant for -/// signature-based replay protection. -contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { +import {RESCUE_ROLE} from "../common/AccessRoles.sol"; + +/// @title BungeeOpenRouter +/// @notice Pull → optional fee → swap/bridge execution without backend signature verification. +/// Fund safety rests on AllowanceHolder's transient allowance scoping (operator + owner + token): +/// only the user whose address was passed to `AllowanceHolder.exec` can authorise a pull of +/// their own funds. The `_msgSender() == user` check in `_pullFromUser` enforces this. +contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { using SafeTransferLib for address; + // ========================================================================= + // Structs + // ========================================================================= + struct InputData { address user; address inputToken; @@ -89,19 +79,19 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // bit 3 : BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK (0x08) — splice finalAmount into bridge calldata // bit 2 : BRIDGE_VALUE_FLAG_BIT_MASK (0x04) — bridge msg.value: bridge.value alone vs finalAmount + bridge.value // bit 1 : BALANCE_FLAG_BIT_MASK (0x02) — swap output: returndata vs balance delta - // bit 0 : FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap (standalone paths only) + // bit 0 : POST_FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap // // Combined values for swap()/swapAndBridge(): // - // flags binary (low byte) postFee? balance-of output? bridge value? - // ───── ────────────────── ──────── ────────────────── ───────────── - // 0x00 00000000 no returndata word bridge.value - // 0x01 00000001 yes returndata word bridge.value - // 0x02 00000010 no balance delta on outputToken bridge.value - // 0x03 00000011 yes balance delta on outputToken bridge.value - // 0x04 00000100 no returndata word finalAmount + bridge.value + // flags binary (low byte) postFee? balance-of output? bridge value? + // ───── ────────────────── ──────── ────────────────── ───────────── + // 0x00 00000000 no returndata word bridge.value + // 0x01 00000001 yes returndata word bridge.value + // 0x02 00000010 no balance delta on outputToken bridge.value + // 0x03 00000011 yes balance delta on outputToken bridge.value + // 0x04 00000100 no returndata word finalAmount + bridge.value // - // FEE_FLAG_BIT_MASK selects bit 0 — fee timing. + // POST_FEE_FLAG_BIT_MASK selects bit 0 — fee timing. // Cleared — pull → deduct fee from input token → swap remainder. // Set — pull → swap full input → deduct fee from output token (after minOutput check on swap result). // @@ -119,7 +109,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. - uint256 internal constant FEE_FLAG_BIT_MASK = 0x01; + uint256 internal constant POST_FEE_FLAG_BIT_MASK = 0x01; /// @dev Bit mask 0x02: measure swap output by balance delta when `(flags & mask) != 0`; clear = returndata word. uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; @@ -141,7 +131,6 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // ========================================================================= error SwapOutputInsufficient(); - error InsufficientFunds(); error InvalidExecution(); error CallerNotSignedUser(); error InsufficientMsgValue(); @@ -155,38 +144,44 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { // Events // ========================================================================= - event RequestExecuted(bytes32 indexed requestHash); + event RequestExecuted(bytes32 indexed quoteId); // ========================================================================= // Constructor // ========================================================================= - constructor(address _owner) Ownable(_owner) {} + /// @notice Deploys the router and grants `RESCUE_ROLE` to `_owner`. + /// @param _owner Initial contract owner and rescue-role holder. + constructor(address _owner) AccessControl(_owner) { + _grantRole(RESCUE_ROLE, _owner); + } + /// @notice Accepts native ETH forwarded with bridge/swap calls. receive() external payable {} // ========================================================================= - // External: standalone swap + // External functions // ========================================================================= /** * @notice Pull → optional pre/post fee → swap. - * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. - * @param receiver Address that ultimately receives the swap output (net of any post-swap fee). - * For pre-fee / no-fee: the swap router must be instructed (via `swapCallData`) to send - * tokens directly to `receiver`; the contract never holds the output. - * For post-fee: tokens land at this contract, fee is deducted, net is forwarded to `receiver`. - * @param flags Packed flags; OR masks then test with `flags & MASK != 0`. Masks: `FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` (0x02). - * Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), - * `2` (pre-fee, balance delta), `3` (post-fee, balance delta). - * @param fee Set `amount` to 0 to skip fee collection. - * @dev `minOutput` is the minimum gross amount coming out of the swap (before any output-token fee). - * It is enforced immediately after `_execSwap`, then post-swap fee (if any) is collected. - * Pre-fee paths take the input-side fee before the swap; `minOutput` still guards the swap outcome. - * Bits are read with bitwise AND against each mask; omitting both masks ⇒ pre-fee + returndata. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param input User, input token, and pull amount. + * @param receiver Address that ultimately receives the swap output (net of any post-swap fee). For pre-fee / no-fee: the swap router must + * be instructed (via `swapCallData`) to send tokens directly to `receiver`; the contract never holds the output. For post-fee: tokens land + * at this contract, fee is deducted, net is forwarded to `receiver`. + * @param flags Packed flags; OR masks then test with `flags & MASK != 0`. Masks: `POST_FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` + * (0x02). Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), `2` (pre-fee, balance delta), `3` (post-fee, balance delta). + * @param fee Set `amount` to 0 to skip fee collection. + * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @return finalAmount Gross swap output before any post-swap fee; net delivered to `receiver` on post-fee paths. + * @dev `minOutput` is the minimum gross amount coming out of the swap (before any output-token fee). It is enforced immediately after + * `_execSwap`, then post-swap fee (if any) is collected. Pre-fee paths take the input-side fee before the swap; `minOutput` still guards the + * swap outcome. */ function swap( - bytes32 requestHash, + bytes32 quoteId, InputData calldata input, address receiver, uint256 flags, @@ -203,24 +198,19 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { _pullFromUser(input.inputToken, input.user, input.inputAmount); - // Check fee amount first: flag bit is only read when a fee is actually present. - // FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output + // POST_FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output bool hasFee = fee.amount != 0; - /// @dev if hasFee is false, we short-circuit and flag check wont execute at runtime saving gas - bool postFee = hasFee && flags & FEE_FLAG_BIT_MASK != 0; + bool postFee = hasFee && flags & POST_FEE_FLAG_BIT_MASK != 0; uint256 swapInput = input.inputAmount; - // collect pre-swap fee if (hasFee && !postFee) { uint256 feeAmount = fee.amount; - if (feeAmount > swapInput) revert InsufficientFunds(); CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); unchecked { swapInput -= feeAmount; } } - // approve swap router if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); } @@ -233,35 +223,33 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { finalAmount = _execSwap(swapData, swapCallData, flags & BALANCE_FLAG_BIT_MASK != 0, outputReceiver); if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); - // collect post-swap fee and forward net to receiver if (postFee) { uint256 feeAmount = fee.amount; - if (feeAmount > finalAmount) revert InsufficientFunds(); CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); unchecked { finalAmount -= feeAmount; } - // Tokens are at this contract; transfer net output to receiver CurrencyLib.transfer(swapData.outputToken, receiver, finalAmount); } // Pre-fee / no-fee: tokens were sent directly to `receiver` by the swap router; nothing to transfer - emit RequestExecuted(requestHash); + emit RequestExecuted(quoteId); } - // ========================================================================= - // External: swap + bridge - // ========================================================================= - /** * @notice Pull → optional pre/post swap fee → swap → bridge with runtime amount splicing. - * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. - * @param flags Same packing as `swap`; additionally bit 2 forwards final amount as bridge msg.value. - * @param fee Set `amount` to 0 to skip fee collection. - * @dev Same `minOutput` rule as `swap`: validated on gross `_execSwap` output, then optional output fee applies. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param input User, input token, and pull amount. + * @param flags Same packing as `swap`; bits 2–3 also control bridge `msg.value` and calldata splicing. + * @param fee Set `amount` to 0 to skip fee collection. + * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. + * @param bridgeCallData Bridge calldata; optionally spliced with swap output per `flags`. + * @dev Same `minOutput` rule as `swap`: validated on gross `_execSwap` output, then optional output fee applies. */ function swapAndBridge( - bytes32 requestHash, + bytes32 quoteId, InputData calldata input, uint256 flags, FeeData calldata fee, @@ -277,85 +265,25 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { revert InvalidExecution(); } - uint256 finalAmount = _swapAndBridgeSwap(input, flags, fee, swapData, swapCallData); - - _finishSwapAndBridge(swapData.outputToken, finalAmount, bridgeData, bridgeCallData, flags); - emit RequestExecuted(requestHash); + uint256 finalAmount = _swapBeforeBridge(input, flags, fee, swapData, swapCallData); + _doBridge(swapData.outputToken, finalAmount, bridgeData, bridgeCallData, flags); + emit RequestExecuted(quoteId); } - function _swapAndBridgeSwap( - InputData calldata input, - uint256 flags, - FeeData calldata fee, - SwapData calldata swapData, - bytes calldata swapCallData - ) internal returns (uint256 finalAmount) { - _pullFromUser(input.inputToken, input.user, input.inputAmount); - bool postFee; - { - // Check fee amount first: flag bit is only read when a fee is actually present. - // FEE_FLAG_BIT_MASK: same semantics as standalone `swap` (fee from input vs output token) - uint256 feeAmount = fee.amount; - postFee = feeAmount != 0 && flags & FEE_FLAG_BIT_MASK != 0; - uint256 swapInput = input.inputAmount; - - if (feeAmount != 0 && !postFee) { - if (feeAmount > swapInput) revert InsufficientFunds(); - CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); - unchecked { - swapInput -= feeAmount; - } - } - - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); - } - } - - // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` - // Swap output always lands at this contract regardless of fee timing — tokens must be here for bridging. - bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; - finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, address(this)); - if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); - - if (postFee) { - uint256 feeAmount = fee.amount; - if (feeAmount > finalAmount) revert InsufficientFunds(); - CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); - unchecked { - finalAmount -= feeAmount; - } - } - } - - function _finishSwapAndBridge( - address finalToken, - uint256 finalAmount, - BridgeData calldata bridgeData, - bytes calldata bridgeCallData, - uint256 flags - ) internal { - _doBridge(finalToken, finalAmount, bridgeData, bridgeCallData, flags); - } - - // ========================================================================= - // External: simple bridge path (no swap) - // ========================================================================= - /** * @notice Pull → optional pre-bridge fee → bridge, with no swap step. - * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. - * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is - * fully knowable by the caller before signing. The caller must therefore - * bake the correct amount directly into `bridgeCallData` and set - * `bridgeData.value` to the desired `msg.value` for the bridge call. - * No runtime calldata splicing is performed. - * - * The caller MUST route through `AllowanceHolder.exec` for ERC-20 - * inputs so that `_msgSender()` resolves to `input.user`. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param input User, input token, and pull amount. + * @param fee Pre-bridge fee taken from the input token; set `amount` to 0 to skip. + * @param bridgeData Bridge target, approval spender, and `msg.value` for the bridge call. + * @param bridgeCallData Calldata forwarded to `bridgeData.target` (amount must be baked in by the caller). + * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is fully knowable by the caller before signing. The caller must + * therefore bake the correct amount directly into `bridgeCallData` and set `bridgeData.value` to the desired `msg.value` for the bridge + * call. No runtime calldata splicing is performed. The caller MUST route through `AllowanceHolder.exec` for ERC-20 inputs so that + * `_msgSender()` resolves to `input.user`. */ function bridge( - bytes32 requestHash, + bytes32 quoteId, InputData calldata input, FeeData calldata fee, BridgeData calldata bridgeData, @@ -365,19 +293,13 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { revert InvalidExecution(); } - // 1. pull funds from user via AllowanceHolder _pullFromUser(input.inputToken, input.user, input.inputAmount); - // 2. optional pre-bridge fee; track net amount for approval uint256 feeAmount = fee.amount; if (feeAmount != 0) { - if (feeAmount > input.inputAmount) { - revert InsufficientFunds(); - } CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); } - // 3. optional approval to bridge spender for the net amount (inputAmount - feeAmount) if (bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 netAmount; unchecked { @@ -386,58 +308,95 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, netAmount); } - // 4. bridge call — data and value are pre-encoded by the caller _doCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); - emit RequestExecuted(requestHash); + emit RequestExecuted(quoteId); } - // ========================================================================= - // External: modular path - // ========================================================================= - /** - * @notice Runs a sequence of generic actions with optional returndata - * splicing between steps. No signature verification. - * @param requestHash Caller-defined correlation id logged in `RequestExecuted`. + * @notice Runs a sequence of generic actions with optional returndata splicing between steps. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param actions Ordered actions; each may splice bytes from a prior action's returndata into its calldata. + * @return results Per-action returndata when the action's `actionInfo` store-result bit is set. */ - function performModularExecution(bytes32 requestHash, Action[] calldata actions) + function performActions(bytes32 quoteId, Action[] calldata actions) external payable returns (bytes[] memory results) { results = _performActions(actions); - emit RequestExecuted(requestHash); + emit RequestExecuted(quoteId); } // ========================================================================= - // Internal: swap / fee / bridge helpers + // Internal functions // ========================================================================= + // + // swap / bridge — orchestration is inline in the external functions; they use + // _pullFromUser, _execSwap (swap), and _doCallCalldata (bridge) from common below. - /// @dev Execute swap; output measured via returndata word or output-token balance delta. - /// useBalanceOf=true: measure output as (balance after - balance before) at `outputReceiver`. - /// useBalanceOf=false: decode output from returndata at swapData.returnDataWordOffset. - /// `outputReceiver` must be `address(this)` when tokens are expected at the contract - /// (post-swap fee path, bridge path) or `user` when the swap router sends directly to them - /// (pre-swap fee / no-fee standalone swap). - function _execSwap( + // ------------------------------------- + // swapAndBridge internal functions + // ------------------------------------- + + /** + * @dev Pull, optional pre/post swap fee, and swap for `swapAndBridge`. Swap output always remains at `address(this)` for bridging. + * @param input User, input token, and pull amount. + * @param flags Fee timing and swap output measurement flags (same as `swap`). + * @param fee Fee receiver and amount; `amount == 0` skips fee collection. + * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @return finalAmount Swap output net of any post-swap fee, ready for `_doBridge`. + */ + function _swapBeforeBridge( + InputData calldata input, + uint256 flags, + FeeData calldata fee, SwapData calldata swapData, - bytes calldata swapCallData, - bool useBalanceOf, - address outputReceiver + bytes calldata swapCallData ) internal returns (uint256 finalAmount) { - if (useBalanceOf) { - // Balance delta mode: snapshot before, call, measure delta at the expected recipient - uint256 before = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver); - _doCallCalldata(swapData.target, swapData.value, swapCallData, false); - finalAmount = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver) - before; - } else { - // Returndata mode: decode output from a specific word in returndata - bytes memory ret = _doCallCalldata(swapData.target, swapData.value, swapCallData, true); - finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); + _pullFromUser(input.inputToken, input.user, input.inputAmount); + bool postFee; + { + // POST_FEE_FLAG_BIT_MASK: same semantics as standalone `swap` (fee from input vs output token) + uint256 feeAmount = fee.amount; + postFee = feeAmount != 0 && flags & POST_FEE_FLAG_BIT_MASK != 0; + uint256 swapInput = input.inputAmount; + + if (feeAmount != 0 && !postFee) { + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + unchecked { + swapInput -= feeAmount; + } + } + + if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + } + } + + // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` + // Swap output always lands at this contract regardless of fee timing — tokens must be here for bridging. + bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; + finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, address(this)); + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + + if (postFee) { + uint256 feeAmount = fee.amount; + CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); + unchecked { + finalAmount -= feeAmount; + } } } - /// @dev Splice finalAmount into bridge calldata, approve, and call bridge target. + /** + * @dev Splice `amount` into bridge calldata when flagged, approve the bridge spender, and call the bridge target. + * @param token ERC-20 bridged (or native sentinel); used for approval only. + * @param amount Post-swap token amount spliced into calldata and/or forwarded as `msg.value`. + * @param bd Bridge target, approval spender, and static `msg.value` addend. + * @param bridgeCallData Base bridge calldata; copied to memory when splicing is required. + * @param flags Bridge splice position, `msg.value` composition, and related bit flags. + */ function _doBridge( address token, uint256 amount, @@ -460,55 +419,16 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { _doCall(bd.target, bridgeValue, bData); } - // ========================================================================= - // Internal: AllowanceHolder pull - // ========================================================================= + // -------------------------------------- + // performActions internal functions + // -------------------------------------- /** - * @notice Pulls `amount` of `token` from `user` into this contract. - * @dev For ERC20: enforces `_msgSender() == user` (caller must have routed - * through `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. - * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea - * For native ETH: ETH must already be present as msg.value; we simply - * verify sufficient value was forwarded. No AH call is needed. + * @dev Executes `actions` in order, applying returndata splices before each call. `actionInfo` layout: bits 0–7 call type (`CallType`), bit 8 + * store returndata, bits 16+ target address. `splices[j]` packs source index, src/dst byte offsets, and length. + * @param actions Ordered list of actions to run. + * @return results Stored returndata per action when the store-result bit is set. */ - function _pullFromUser(address token, address user, uint256 amount) internal { - if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { - // ETH is already sent as msg.value directly to this contract. - if (msg.value < amount) { - revert InsufficientMsgValue(); - } - return; - } - - if (_msgSender() != user) { - revert CallerNotSignedUser(); - } - - address allowanceHolder = address(ALLOWANCE_HOLDER); - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(add(0x80, ptr), amount) - mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding - // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which - // shifts the 20-byte address out of place and corrupts the calldata token. Same as - // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. - mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) - mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding - - if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { - let p := mload(0x40) - returndatacopy(p, 0x00, returndatasize()) - revert(p, returndatasize()) - } - } - } - - // ========================================================================= - // Internal: modular action loop - // ========================================================================= - function _performActions(Action[] calldata actions) internal returns (bytes[] memory results) { uint256 actionsLength = actions.length; results = new bytes[](actionsLength); @@ -582,10 +502,84 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } } - // ========================================================================= - // Internal: simple call dispatcher - // ========================================================================= + // ------------------------------- + // Common internal functions + // ------------------------------- + + /** + * @dev Pulls `amount` of `token` from `user` into this contract. For ERC20: enforces `_msgSender() == user` (caller must have routed through + * `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. AH selector: + * transferFrom(address,address,address,uint256) = 0x15dacbea. For native ETH: ETH must already be present as msg.value; verify sufficient + * value was forwarded. No AH call is needed. + * @param token Input token or `CurrencyLib.NATIVE_TOKEN_ADDRESS`. + * @param user Owner whose AllowanceHolder-scoped allowance is consumed. + * @param amount Tokens or wei to pull. + */ + function _pullFromUser(address token, address user, uint256 amount) internal { + if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { + if (msg.value < amount) { + revert InsufficientMsgValue(); + } + return; + } + + if (_msgSender() != user) { + revert CallerNotSignedUser(); + } + + address allowanceHolder = address(ALLOWANCE_HOLDER); + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(add(0x80, ptr), amount) + mstore(add(0x60, ptr), address()) + mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding + // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which + // shifts the 20-byte address out of place and corrupts the calldata token. Same as + // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. + mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) + mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding + + if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { + let p := mload(0x40) + returndatacopy(p, 0x00, returndatasize()) + revert(p, returndatasize()) + } + } + } + + /** + * @dev Executes the swap call and returns the output amount. `useBalanceOf=true`: measure output as (balance after − balance before) at + * `outputReceiver`. `useBalanceOf=false`: decode output from returndata at `swapData.returnDataWordOffset`. `outputReceiver` must be + * `address(this)` when tokens are expected at the contract (post-swap fee path, bridge path) or the end user when the router sends directly + * to them. + * @param swapData Swap target, value, output token, and returndata layout. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @param useBalanceOf When true, use balance delta instead of returndata decoding. + * @param outputReceiver Account whose output-token balance is measured or credited. + * @return finalAmount Gross swap output amount. + */ + function _execSwap( + SwapData calldata swapData, + bytes calldata swapCallData, + bool useBalanceOf, + address outputReceiver + ) internal returns (uint256 finalAmount) { + if (useBalanceOf) { + uint256 before = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver); + _doCallCalldata(swapData.target, swapData.value, swapCallData, false); + finalAmount = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver) - before; + } else { + bytes memory ret = _doCallCalldata(swapData.target, swapData.value, swapCallData, true); + finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); + } + } + /** + * @dev Low-level `call` with bubbled revert data on failure. + * @param target Call recipient. + * @param value Wei forwarded with the call. + * @param data ABI-encoded calldata in memory. + */ function _doCall(address target, uint256 value, bytes memory data) internal { bool success; assembly ("memory-safe") { @@ -605,6 +599,14 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } } + /** + * @dev Low-level `call` using calldata copied to memory; optionally captures returndata. + * @param target Call recipient. + * @param value Wei forwarded with the call. + * @param data Calldata slice forwarded to `target`. + * @param storeResult When true, copy returndata into memory even on success. + * @return ret Returndata when `storeResult` is true or the call reverts (revert bubbles). + */ function _doCallCalldata(address target, uint256 value, bytes calldata data, bool storeResult) internal returns (bytes memory ret) @@ -633,6 +635,12 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { } } + /** + * @dev Reads the 32-byte word at `wordOffset` from ABI-encoded `ret` (word index, not byte offset). + * @param ret Return blob from a prior call. + * @param wordOffset Zero-based index of the 32-byte word to load. + * @return word Decoded amount or value at that offset. + */ function _decodeReturnWord(bytes memory ret, uint256 wordOffset) internal pure returns (uint256 word) { uint256 offset = wordOffset * 32; if (offset + 32 > ret.length) revert ReturnDataOutOfBounds(); @@ -648,7 +656,7 @@ contract BungeeOpenRouterV2Unchecked is Ownable, AllowanceHolderContext { * @param rescueTo_ The address where rescued tokens need to be sent. * @param amount_ The amount of tokens to be rescued. */ - function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyOwner { + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyRole(RESCUE_ROLE) { RescueFundsLib.rescueFunds(token_, rescueTo_, amount_); } } From 9a19a3c9a785cf1fedfd22acaf12ffe8f6c722e1 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 17:14:58 +0530 Subject: [PATCH 50/69] build: change solc version --- foundry.toml | 2 +- src/combined/BungeeOpenRouterV2.sol | 2 +- src/common/AccessRoles.sol | 4 ++ src/common/OpenRouterAuthBase.sol | 2 +- .../allowance/AllowanceHolderContext.sol | 2 +- src/common/interfaces/IAllowanceHolder.sol | 2 +- src/common/lib/AuthenticationLib.sol | 2 +- src/common/lib/BytesSpliceLib.sol | 2 +- src/common/lib/CurrencyLib.sol | 2 +- src/common/lib/RescueFundsLib.sol | 2 +- src/common/utils/AccessControl.sol | 46 +++++++++++++++++++ src/common/utils/Ownable.sol | 2 +- .../AcrossERC20AmountManipulator.sol | 2 +- src/manipulators/MathManipulator.sol | 2 +- src/minimal/BungeeOpenRouterMinimal.sol | 2 +- src/minimal/BungeeOpenRouterMinimalAH.sol | 2 +- src/modular/BungeeOpenRouterModular.sol | 2 +- src/modular/BungeeOpenRouterModularAH.sol | 2 +- src/monolithic/BungeeOpenRouter.sol | 2 +- src/monolithic/BungeeOpenRouterAH.sol | 2 +- 20 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 src/common/AccessRoles.sol create mode 100644 src/common/utils/AccessControl.sol diff --git a/foundry.toml b/foundry.toml index 34b3732..bb17d99 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ src = "src" out = "out" libs = ["lib"] -solc_version = "0.8.25" +solc_version = "0.8.34" evm_version = "cancun" optimizer = true optimizer_runs = 2_000 diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol index 67fe166..f8c002d 100644 --- a/src/combined/BungeeOpenRouterV2.sol +++ b/src/combined/BungeeOpenRouterV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; diff --git a/src/common/AccessRoles.sol b/src/common/AccessRoles.sol new file mode 100644 index 0000000..0039d01 --- /dev/null +++ b/src/common/AccessRoles.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +bytes32 constant RESCUE_ROLE = keccak256("RESCUE_ROLE"); diff --git a/src/common/OpenRouterAuthBase.sol b/src/common/OpenRouterAuthBase.sol index dbf416b..db7316b 100644 --- a/src/common/OpenRouterAuthBase.sol +++ b/src/common/OpenRouterAuthBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {Ownable} from "./utils/Ownable.sol"; import {AuthenticationLib} from "./lib/AuthenticationLib.sol"; diff --git a/src/common/allowance/AllowanceHolderContext.sol b/src/common/allowance/AllowanceHolderContext.sol index 2ab3f2b..34ed2db 100644 --- a/src/common/allowance/AllowanceHolderContext.sol +++ b/src/common/allowance/AllowanceHolderContext.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {ALLOWANCE_HOLDER} from "../interfaces/IAllowanceHolder.sol"; diff --git a/src/common/interfaces/IAllowanceHolder.sol b/src/common/interfaces/IAllowanceHolder.sol index a941f77..1ec809f 100644 --- a/src/common/interfaces/IAllowanceHolder.sol +++ b/src/common/interfaces/IAllowanceHolder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.25; +pragma solidity 0.8.34; // @dev Mainnet AllowanceHolder address. Same address is used for every chain // on which 0x deploys it via the canonical CREATE2 deployer. See: diff --git a/src/common/lib/AuthenticationLib.sol b/src/common/lib/AuthenticationLib.sol index d1bfdde..0a65cbd 100644 --- a/src/common/lib/AuthenticationLib.sol +++ b/src/common/lib/AuthenticationLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @title AuthenticationLib /// @notice Personal-sign style signature recovery, ported from diff --git a/src/common/lib/BytesSpliceLib.sol b/src/common/lib/BytesSpliceLib.sol index 8426d28..fc6a890 100644 --- a/src/common/lib/BytesSpliceLib.sol +++ b/src/common/lib/BytesSpliceLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @title BytesSpliceLib /// @notice Generalisation of the in-place calldata patching used in diff --git a/src/common/lib/CurrencyLib.sol b/src/common/lib/CurrencyLib.sol index d6df584..7208f66 100644 --- a/src/common/lib/CurrencyLib.sol +++ b/src/common/lib/CurrencyLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; diff --git a/src/common/lib/RescueFundsLib.sol b/src/common/lib/RescueFundsLib.sol index a18b950..221c215 100644 --- a/src/common/lib/RescueFundsLib.sol +++ b/src/common/lib/RescueFundsLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; diff --git a/src/common/utils/AccessControl.sol b/src/common/utils/AccessControl.sol new file mode 100644 index 0000000..3faca7d --- /dev/null +++ b/src/common/utils/AccessControl.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +import {Ownable} from "./Ownable.sol"; + +abstract contract AccessControl is Ownable { + mapping(bytes32 => mapping(address => bool)) private _permits; + + event RoleGranted(bytes32 indexed role, address indexed grantee); + event RoleRevoked(bytes32 indexed role, address indexed revokee); + + error NoPermit(bytes32 role); + + constructor(address owner_) Ownable(owner_) {} + + modifier onlyRole(bytes32 role) { + if (!_permits[role][msg.sender]) revert NoPermit(role); + _; + } + + function grantRole(bytes32 role_, address grantee_) external virtual onlyOwner { + _grantRole(role_, grantee_); + } + + function revokeRole(bytes32 role_, address revokee_) external virtual onlyOwner { + _revokeRole(role_, revokee_); + } + + function hasRole(bytes32 role_, address address_) public view returns (bool) { + return _hasRole(role_, address_); + } + + function _grantRole(bytes32 role_, address grantee_) internal { + _permits[role_][grantee_] = true; + emit RoleGranted(role_, grantee_); + } + + function _revokeRole(bytes32 role_, address revokee_) internal { + _permits[role_][revokee_] = false; + emit RoleRevoked(role_, revokee_); + } + + function _hasRole(bytes32 role_, address address_) internal view returns (bool) { + return _permits[role_][address_]; + } +} diff --git a/src/common/utils/Ownable.sol b/src/common/utils/Ownable.sol index f03d76f..dd83c0b 100644 --- a/src/common/utils/Ownable.sol +++ b/src/common/utils/Ownable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @title Ownable /// @notice Two-step ownership transfer, ported from diff --git a/src/manipulators/AcrossERC20AmountManipulator.sol b/src/manipulators/AcrossERC20AmountManipulator.sol index d99d79d..9df80dc 100644 --- a/src/manipulators/AcrossERC20AmountManipulator.sol +++ b/src/manipulators/AcrossERC20AmountManipulator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @notice Computes the Across output amount that must be spliced into SpokePool.deposit calldata. contract AcrossERC20AmountManipulator { diff --git a/src/manipulators/MathManipulator.sol b/src/manipulators/MathManipulator.sol index 257b800..7879cd5 100644 --- a/src/manipulators/MathManipulator.sol +++ b/src/manipulators/MathManipulator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @notice Generic arithmetic helpers for router calldata splicing. contract MathManipulator { diff --git a/src/minimal/BungeeOpenRouterMinimal.sol b/src/minimal/BungeeOpenRouterMinimal.sol index 86c1bc5..78aad53 100644 --- a/src/minimal/BungeeOpenRouterMinimal.sol +++ b/src/minimal/BungeeOpenRouterMinimal.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; diff --git a/src/minimal/BungeeOpenRouterMinimalAH.sol b/src/minimal/BungeeOpenRouterMinimalAH.sol index fbd0401..f62b112 100644 --- a/src/minimal/BungeeOpenRouterMinimalAH.sol +++ b/src/minimal/BungeeOpenRouterMinimalAH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {BungeeOpenRouterMinimal} from "./BungeeOpenRouterMinimal.sol"; import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; diff --git a/src/modular/BungeeOpenRouterModular.sol b/src/modular/BungeeOpenRouterModular.sol index 14983bc..806a538 100644 --- a/src/modular/BungeeOpenRouterModular.sol +++ b/src/modular/BungeeOpenRouterModular.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; diff --git a/src/modular/BungeeOpenRouterModularAH.sol b/src/modular/BungeeOpenRouterModularAH.sol index e0f37cb..968aa68 100644 --- a/src/modular/BungeeOpenRouterModularAH.sol +++ b/src/modular/BungeeOpenRouterModularAH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {BungeeOpenRouterModular} from "./BungeeOpenRouterModular.sol"; import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; diff --git a/src/monolithic/BungeeOpenRouter.sol b/src/monolithic/BungeeOpenRouter.sol index df0ac22..4ed46bf 100644 --- a/src/monolithic/BungeeOpenRouter.sol +++ b/src/monolithic/BungeeOpenRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; diff --git a/src/monolithic/BungeeOpenRouterAH.sol b/src/monolithic/BungeeOpenRouterAH.sol index 2f0f560..6a1805e 100644 --- a/src/monolithic/BungeeOpenRouterAH.sol +++ b/src/monolithic/BungeeOpenRouterAH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {BungeeOpenRouter} from "./BungeeOpenRouter.sol"; import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; From 419f396038794e2bb6ae46e2f640e88de8f54074 Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 19 May 2026 15:51:26 +0400 Subject: [PATCH 51/69] feat: balance and return data variants --- ...eeOpenRouterV2UncheckedSwapAndBridge.t.sol | 81 ++++++++++++++----- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol b/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol index b3c0efd..8a2a445 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol +++ b/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol @@ -15,48 +15,85 @@ contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2Unche address input; address output; FeeMode feeMode; + bool balanceDelta; uint256 swapInput; uint256 bridgeAmount; } function test_swapAndBridge_noFee_erc20ToNative() public { - _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.None); + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.None, false); } function test_swapAndBridge_noFee_nativeToErc20() public { - _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.None); + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.None, false); } function test_swapAndBridge_noFee_erc20ToErc20() public { - _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.None); + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.None, false); } function test_swapAndBridge_prefee_erc20ToNative() public { - _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Pre); + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Pre, false); } function test_swapAndBridge_prefee_nativeToErc20() public { - _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Pre); + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Pre, false); } function test_swapAndBridge_prefee_erc20ToErc20() public { - _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Pre); + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Pre, false); } function test_swapAndBridge_postfee_erc20ToNative() public { - _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Post); + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Post, false); } function test_swapAndBridge_postfee_nativeToErc20() public { - _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Post); + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Post, false); } function test_swapAndBridge_postfee_erc20ToErc20() public { - _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Post); + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Post, false); } - function _runSwapAndBridge(address input, address output, FeeMode feeMode) internal { - Scenario memory scenario = _scenario(input, output, feeMode); + function test_swapAndBridge_balanceDelta_noFee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.None, true); + } + + function test_swapAndBridge_balanceDelta_noFee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.None, true); + } + + function test_swapAndBridge_balanceDelta_noFee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.None, true); + } + + function test_swapAndBridge_balanceDelta_prefee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Pre, true); + } + + function test_swapAndBridge_balanceDelta_prefee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Pre, true); + } + + function test_swapAndBridge_balanceDelta_prefee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Pre, true); + } + + function test_swapAndBridge_balanceDelta_postfee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Post, true); + } + + function test_swapAndBridge_balanceDelta_postfee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Post, true); + } + + function test_swapAndBridge_balanceDelta_postfee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Post, true); + } + + function _runSwapAndBridge(address input, address output, FeeMode feeMode, bool balanceDelta) internal { + Scenario memory scenario = _scenario(input, output, feeMode, balanceDelta); _fundSwapAndBridge(scenario.input, scenario.output); if (scenario.input != NATIVE_TOKEN) _approveInputToken(INPUT_AMOUNT); @@ -71,7 +108,7 @@ contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2Unche assertEq(bridgeTarget.receivedAmount(), scenario.bridgeAmount); } - function _scenario(address input, address output, FeeMode feeMode) + function _scenario(address input, address output, FeeMode feeMode, bool balanceDelta) internal pure returns (Scenario memory scenario) @@ -79,6 +116,7 @@ contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2Unche scenario.input = input; scenario.output = output; scenario.feeMode = feeMode; + scenario.balanceDelta = balanceDelta; scenario.swapInput = _swapInput(feeMode); scenario.bridgeAmount = _bridgeAmount(feeMode); } @@ -98,7 +136,7 @@ contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2Unche ( keccak256("swap-and-bridge"), Router.InputData({user: USER, inputToken: scenario.input, inputAmount: INPUT_AMOUNT}), - _flags(scenario.output, scenario.feeMode), + _flags(scenario.output, scenario.feeMode, scenario.balanceDelta), _fee(scenario.feeMode), _swapDataWithValue( scenario.input, @@ -106,15 +144,22 @@ contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2Unche SWAP_OUTPUT_AMOUNT, scenario.input == NATIVE_TOKEN ? scenario.swapInput : 0 ), - _swapNoReturnCallData( - scenario.input, scenario.output, scenario.swapInput, SWAP_OUTPUT_AMOUNT, address(router) - ), + _swapCallData(scenario), _bridgeData(scenario.output, 0), _bridgeCallData(scenario.output, 0) ) ); } + function _swapCallData(Scenario memory scenario) internal view returns (bytes memory) { + if (scenario.balanceDelta) { + return _swapNoReturnCallData( + scenario.input, scenario.output, scenario.swapInput, SWAP_OUTPUT_AMOUNT, address(router) + ); + } + return _swapCallData(scenario.input, scenario.output, scenario.swapInput, SWAP_OUTPUT_AMOUNT, address(router)); + } + function _fundSwapAndBridge(address input, address output) internal { _deal(input, USER, INPUT_AMOUNT); _deal(output, address(swapTarget), SWAP_OUTPUT_AMOUNT); @@ -148,8 +193,8 @@ contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2Unche return feeMode == FeeMode.Post ? SWAP_OUTPUT_AMOUNT - FEE_AMOUNT : SWAP_OUTPUT_AMOUNT; } - function _flags(address output, FeeMode feeMode) internal pure returns (uint256) { - uint256 flags = BALANCE_FLAG_BIT_MASK; + function _flags(address output, FeeMode feeMode, bool balanceDelta) internal pure returns (uint256) { + uint256 flags = balanceDelta ? BALANCE_FLAG_BIT_MASK : 0; if (output == NATIVE_TOKEN) flags |= BRIDGE_VALUE_FLAG_BIT_MASK; if (feeMode == FeeMode.Post) flags |= FEE_FLAG_BIT_MASK; return _bridgeAmountSpliceFlags(flags); From 578ef446358050684829f76ea45f43e73b166be6 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 17:28:38 +0530 Subject: [PATCH 52/69] refactor: reorder function params --- src/combined/BungeeOpenRouterV2Unchecked.sol | 74 ++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/combined/BungeeOpenRouterV2Unchecked.sol index 3237fc2..9481b25 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/combined/BungeeOpenRouterV2Unchecked.sol @@ -182,12 +182,12 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { */ function swap( bytes32 quoteId, - InputData calldata input, - address receiver, uint256 flags, + InputData calldata input, FeeData calldata fee, SwapData calldata swapData, - bytes calldata swapCallData + bytes calldata swapCallData, + address receiver ) external payable returns (uint256 finalAmount) { if ( input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0) @@ -196,23 +196,25 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { revert InvalidExecution(); } - _pullFromUser(input.inputToken, input.user, input.inputAmount); + bool postFee = fee.amount != 0 && flags & POST_FEE_FLAG_BIT_MASK != 0; + bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; - // POST_FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output - bool hasFee = fee.amount != 0; - bool postFee = hasFee && flags & POST_FEE_FLAG_BIT_MASK != 0; - uint256 swapInput = input.inputAmount; + { + _pullFromUser(input.inputToken, input.user, input.inputAmount); - if (hasFee && !postFee) { - uint256 feeAmount = fee.amount; - CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); - unchecked { - swapInput -= feeAmount; + // POST_FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output + uint256 swapInput = input.inputAmount; + if (fee.amount != 0 && !postFee) { + uint256 feeAmount = fee.amount; + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + unchecked { + swapInput -= feeAmount; + } } - } - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + } } // Post-fee: swap output lands at this contract so the fee can be deducted before forwarding. @@ -220,7 +222,7 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { address outputReceiver = postFee ? address(this) : receiver; // BALANCE_FLAG_BIT_MASK: unset ⇒ decode output word from returndata; set ⇒ output = delta on outputToken - finalAmount = _execSwap(swapData, swapCallData, flags & BALANCE_FLAG_BIT_MASK != 0, outputReceiver); + finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, outputReceiver); if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); if (postFee) { @@ -250,8 +252,8 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { */ function swapAndBridge( bytes32 quoteId, - InputData calldata input, uint256 flags, + InputData calldata input, FeeData calldata fee, SwapData calldata swapData, bytes calldata swapCallData, @@ -265,8 +267,8 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { revert InvalidExecution(); } - uint256 finalAmount = _swapBeforeBridge(input, flags, fee, swapData, swapCallData); - _doBridge(swapData.outputToken, finalAmount, bridgeData, bridgeCallData, flags); + uint256 finalAmount = _swapBeforeBridge(flags, input, fee, swapData, swapCallData); + _doBridge(swapData.outputToken, finalAmount, flags, bridgeData, bridgeCallData); emit RequestExecuted(quoteId); } @@ -348,8 +350,8 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { * @return finalAmount Swap output net of any post-swap fee, ready for `_doBridge`. */ function _swapBeforeBridge( - InputData calldata input, uint256 flags, + InputData calldata input, FeeData calldata fee, SwapData calldata swapData, bytes calldata swapCallData @@ -393,30 +395,30 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { * @dev Splice `amount` into bridge calldata when flagged, approve the bridge spender, and call the bridge target. * @param token ERC-20 bridged (or native sentinel); used for approval only. * @param amount Post-swap token amount spliced into calldata and/or forwarded as `msg.value`. - * @param bd Bridge target, approval spender, and static `msg.value` addend. + * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. * @param bridgeCallData Base bridge calldata; copied to memory when splicing is required. * @param flags Bridge splice position, `msg.value` composition, and related bit flags. */ function _doBridge( address token, uint256 amount, - BridgeData calldata bd, - bytes calldata bridgeCallData, - uint256 flags + uint256 flags, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData ) internal { - bytes memory bData = bridgeCallData; + bytes memory _bridgeCallData = bridgeCallData; if (flags & BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK != 0) { uint256 position = flags >> BRIDGE_AMOUNT_POSITION_SHIFT & BRIDGE_AMOUNT_POSITION_MASK; - BytesSpliceLib.spliceWord({data: bData, position: position, word: amount}); + BytesSpliceLib.spliceWord({data: _bridgeCallData, position: position, word: amount}); } - if (bd.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(token, bd.approvalSpender, amount); + if (bridgeData.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { + SafeTransferLib.safeApproveWithRetry(token, bridgeData.approvalSpender, amount); } // when set, forward amount as msg.value for native-token bridges - uint256 bridgeValue = flags & BRIDGE_VALUE_FLAG_BIT_MASK != 0 ? amount + bd.value : bd.value; - _doCall(bd.target, bridgeValue, bData); + uint256 bridgeValue = flags & BRIDGE_VALUE_FLAG_BIT_MASK != 0 ? amount + bridgeData.value : bridgeData.value; + _doCall(bridgeData.target, bridgeValue, _bridgeCallData); } // -------------------------------------- @@ -652,12 +654,12 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { /** * @notice Rescues funds from the contract if they are locked by mistake. - * @param token_ The address of the token contract. - * @param rescueTo_ The address where rescued tokens need to be sent. - * @param amount_ The amount of tokens to be rescued. + * @param token The address of the token contract. + * @param rescueTo The address where rescued tokens need to be sent. + * @param amount The amount of tokens to be rescued. */ - function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyRole(RESCUE_ROLE) { - RescueFundsLib.rescueFunds(token_, rescueTo_, amount_); + function rescueFunds(address token, address rescueTo, uint256 amount) external onlyRole(RESCUE_ROLE) { + RescueFundsLib.rescueFunds(token, rescueTo, amount); } } From d84d648158ef10e384c026dc61b82966166db339 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 17:32:36 +0530 Subject: [PATCH 53/69] refactor: remove old contracts, rename router, move to src root --- ...erV2Unchecked.sol => BungeeOpenRouter.sol} | 14 +- src/combined/BungeeOpenRouterV2.sol | 420 ------------------ src/minimal/BungeeOpenRouterMinimal.sol | 99 ----- src/minimal/BungeeOpenRouterMinimalAH.sol | 31 -- src/modular/BungeeOpenRouterModular.sol | 144 ------ src/modular/BungeeOpenRouterModularAH.sol | 45 -- src/monolithic/BungeeOpenRouter.sol | 190 -------- src/monolithic/BungeeOpenRouterAH.sol | 69 --- 8 files changed, 7 insertions(+), 1005 deletions(-) rename src/{combined/BungeeOpenRouterV2Unchecked.sol => BungeeOpenRouter.sol} (98%) delete mode 100644 src/combined/BungeeOpenRouterV2.sol delete mode 100644 src/minimal/BungeeOpenRouterMinimal.sol delete mode 100644 src/minimal/BungeeOpenRouterMinimalAH.sol delete mode 100644 src/modular/BungeeOpenRouterModular.sol delete mode 100644 src/modular/BungeeOpenRouterModularAH.sol delete mode 100644 src/monolithic/BungeeOpenRouter.sol delete mode 100644 src/monolithic/BungeeOpenRouterAH.sol diff --git a/src/combined/BungeeOpenRouterV2Unchecked.sol b/src/BungeeOpenRouter.sol similarity index 98% rename from src/combined/BungeeOpenRouterV2Unchecked.sol rename to src/BungeeOpenRouter.sol index 9481b25..60abf98 100644 --- a/src/combined/BungeeOpenRouterV2Unchecked.sol +++ b/src/BungeeOpenRouter.sol @@ -3,13 +3,13 @@ pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; -import {AccessControl} from "../common/utils/AccessControl.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; -import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; -import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; -import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; -import {RescueFundsLib} from "../common/lib/RescueFundsLib.sol"; -import {RESCUE_ROLE} from "../common/AccessRoles.sol"; +import {AccessControl} from "./common/utils/AccessControl.sol"; +import {AllowanceHolderContext} from "./common/allowance/AllowanceHolderContext.sol"; +import {ALLOWANCE_HOLDER} from "./common/interfaces/IAllowanceHolder.sol"; +import {BytesSpliceLib} from "./common/lib/BytesSpliceLib.sol"; +import {CurrencyLib} from "./common/lib/CurrencyLib.sol"; +import {RescueFundsLib} from "./common/lib/RescueFundsLib.sol"; +import {RESCUE_ROLE} from "./common/AccessRoles.sol"; /// @title BungeeOpenRouter /// @notice Pull → optional fee → swap/bridge execution without backend signature verification. diff --git a/src/combined/BungeeOpenRouterV2.sol b/src/combined/BungeeOpenRouterV2.sol deleted file mode 100644 index f8c002d..0000000 --- a/src/combined/BungeeOpenRouterV2.sol +++ /dev/null @@ -1,420 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; -import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; -import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; -import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; - -/// @title BungeeOpenRouterV2 -/// @notice Combined open-router that exposes two execution paths behind a -/// single signature-verified, AllowanceHolder-based fund pull: -/// -/// 1. `performExecution` — monolithic path. The signed payload describes -/// every step explicitly: pull, optional pre-swap fee, optional swap, -/// optional post-swap fee, bridge call with multi-position amount -/// splicing. Suitable for the vast majority of routes. -/// -/// 2. `performModularExecution` — generic action loop. Each `Action` -/// carries packed call metadata and packed splices that copy byte -/// ranges from any earlier stored action result into this action's -/// calldata before dispatch. -/// -/// Fund pulls always go through 0x AllowanceHolder (transient-storage -/// allowance). The `_msgSender() == user` guard ensures the AH -/// ephemeral allowance (keyed by operator + owner + token) belongs to -/// the user named in the signed payload. -/// -/// @dev Both entrypoints verify a personal_sign signature over -/// `keccak256(abi.encode(chainid, address(this), exec))` and consume a -/// single-use nonce, matching the `Solver` / `StakedRouterReceiver` -/// authentication model. -contract BungeeOpenRouterV2 is OpenRouterAuthBase, AllowanceHolderContext { - using SafeTransferLib for address; - - // ========================================================================= - // Monolithic execution types - // ========================================================================= - - /// @notice Who is sending funds and how much. - struct InputData { - address user; - address inputToken; - uint256 inputAmount; - } - - /// @notice Optional fee. Set `receiver` to address(0) and `amount` to 0 to skip. - struct FeeData { - address receiver; - uint256 amount; - } - - /// @notice Optional swap step. Set `target` to address(0) to skip entirely. - struct SwapData { - address target; - address approvalSpender; // 0 to skip ERC20 approval before swap - address outputToken; // token used for post-fee transfer / bridge approval - uint256 value; // ETH forwarded to the swap target - uint256 minOutput; // minimum decoded output; reverts if not met - bytes data; - uint256 returnDataWordOffset; - } - - /// @notice Mandatory bridge call. `amountPositions` lists every byte offset - /// in `data` where the final post-fee amount must be written. - struct BridgeData { - address target; - address approvalSpender; // 0 to skip ERC20 approval before bridge - uint256 value; // ETH forwarded to the bridge target - bytes data; - uint256[] amountPositions; - // when true, bridge.value is ignored and finalAmount is forwarded as - // msg.value instead — needed for native-token bridges (e.g. Arbitrum inbox) - // where the bridged amount is only known at runtime. - bool useFinalAmountAsValue; - } - - /// @notice Signed payload for the monolithic execution path. - /// @dev Digest: keccak256(abi.encode(block.chainid, address(this), exec)). - struct MonolithicExecution { - InputData input; - FeeData preFee; // taken in inputToken before swap - SwapData swap; - FeeData postFee; // taken in finalToken after swap - BridgeData bridge; - uint256 nonce; - uint256 deadline; - } - - // ========================================================================= - // Modular execution types - // ========================================================================= - - enum CallType { - CALL, - STATICCALL, - CALL_WITH_NATIVE - } - - /// @notice One step in the modular execution pipeline. - /// @dev `actionInfo` packs call type in bits [0:8), store-result flag in - /// bits [8:16), and target address in bits [16:176). - struct Action { - uint256 actionInfo; - bytes data; - uint256[] splices; - } - - /// @notice Signed payload for the modular execution path. - struct ModularExecution { - Action[] actions; - uint256 nonce; - uint256 deadline; - } - - // ========================================================================= - // Errors - // ========================================================================= - - error SwapOutputInsufficient(); - error InsufficientFunds(); - error InvalidExecution(); - error CallerNotSignedUser(); - error InsufficientMsgValue(); - error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); - error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); - error CallFailed(uint256 actionIndex, bytes returndata); - error MissingNativeValue(uint256 actionIndex); - error ReturnDataOutOfBounds(); - - // ========================================================================= - // Constructor - // ========================================================================= - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - // ========================================================================= - // External: monolithic path - // ========================================================================= - - /** - * @notice Executes a monolithic signed payload: pull funds via AH, optional - * pre-swap fee, optional swap, optional post-swap fee, bridge call - * with multi-position amount splicing. - * @dev Anyone may call; security is the backend signature + single-use nonce. - * The caller MUST route through `AllowanceHolder.exec` so that - * `_msgSender()` resolves to `exec.input.user`. - */ - function performExecution(MonolithicExecution calldata exec, bytes calldata signature) external payable { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _runMonolithic(exec); - } - - // ========================================================================= - // External: modular path - // ========================================================================= - - /** - * @notice Executes a signed sequence of generic actions with optional - * returndata splicing between steps. - * @dev The signed digest covers the entire action set, so the caller cannot - * reorder, retarget, or strip splices from any action. - */ - function performModularExecution(ModularExecution calldata exec, bytes calldata signature) - external - payable - returns (bytes[] memory results) - { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - results = _performActions(exec.actions); - } - - // ========================================================================= - // Internal: monolithic pipeline - // ========================================================================= - - function _runMonolithic(MonolithicExecution calldata exec) internal { - if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { - revert InvalidExecution(); - } - - // 1. pull funds from user via AllowanceHolder - _pullFromUser(exec.input.inputToken, exec.input.user, exec.input.inputAmount); - - // 2. optional pre-swap fee in input token - if (exec.preFee.amount != 0) { - CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); - } - - // 3. optional swap, accounted via decoded returndata - address finalToken; - uint256 finalAmount; - if (exec.swap.target != address(0)) { - (finalToken, finalAmount) = _performSwap(exec); - } else { - if (exec.preFee.amount > exec.input.inputAmount) { - revert InsufficientFunds(); - } - finalToken = exec.input.inputToken; - unchecked { - finalAmount = exec.input.inputAmount - exec.preFee.amount; - } - } - - // 4. optional post-swap fee in final token - if (exec.postFee.amount != 0) { - if (exec.postFee.amount > finalAmount) { - revert InsufficientFunds(); - } - CurrencyLib.transfer(finalToken, exec.postFee.receiver, exec.postFee.amount); - unchecked { - finalAmount -= exec.postFee.amount; - } - } - - // 5. splice finalAmount into bridge calldata at every signed offset - bytes memory bridgeData = exec.bridge.data; - BytesSpliceLib.spliceWords({data: bridgeData, positions: exec.bridge.amountPositions, word: finalAmount}); - - // 6. optional approval to bridge spender - if (exec.bridge.approvalSpender != address(0) && finalToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(finalToken, exec.bridge.approvalSpender, finalAmount); - } - - // 7. bridge call, bubbling any revert - // when useFinalAmountAsValue is set, forward finalAmount as msg.value so - // native-token bridges (e.g. Arbitrum inbox) receive the exact bridged amount. - uint256 bridgeValue = exec.bridge.useFinalAmountAsValue ? finalAmount : exec.bridge.value; - _doCall(exec.bridge.target, bridgeValue, bridgeData, false); - } - - /// @dev Swap helper; decodes final amount from a returndata word. - function _performSwap(MonolithicExecution calldata exec) - internal - returns (address finalToken, uint256 finalAmount) - { - if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - uint256 swapInput; - unchecked { - swapInput = exec.input.inputAmount - exec.preFee.amount; - } - SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); - } - - bytes memory ret = _doCall(exec.swap.target, exec.swap.value, exec.swap.data, true); - finalAmount = _decodeReturnWord(ret, exec.swap.returnDataWordOffset); - - if (finalAmount < exec.swap.minOutput) { - revert SwapOutputInsufficient(); - } - - finalToken = exec.swap.outputToken; - } - - // ========================================================================= - // Internal: AllowanceHolder pull - // ========================================================================= - - /** - * @notice Pulls `amount` of `token` from `user` into this contract. - * @dev For ERC20: enforces `_msgSender() == user` (caller must have routed - * through `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. - * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea - * For native ETH: ETH must already be present as msg.value; we simply - * verify sufficient value was forwarded. No AH call is needed. - */ - function _pullFromUser(address token, address user, uint256 amount) internal { - if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { - // ETH is already sent as msg.value directly to this contract. - if (msg.value < amount) { - revert InsufficientMsgValue(); - } - return; - } - - if (_msgSender() != user) { - revert CallerNotSignedUser(); - } - - address allowanceHolder = address(ALLOWANCE_HOLDER); - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(add(0x80, ptr), amount) - mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding - // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which - // shifts the 20-byte address out of place and corrupts the calldata token. Same as - // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. - mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) - mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding - - if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { - let p := mload(0x40) - returndatacopy(p, 0x00, returndatasize()) - revert(p, returndatasize()) - } - } - } - - // ========================================================================= - // Internal: modular action loop - // ========================================================================= - - function _performActions(Action[] calldata actions) internal returns (bytes[] memory results) { - uint256 actionsLength = actions.length; - results = new bytes[](actionsLength); - - for (uint256 i; i < actionsLength;) { - Action calldata action = actions[i]; - bytes memory callData = action.data; - - uint256 splicesLength = action.splices.length; - for (uint256 j; j < splicesLength;) { - uint256 spliceInfo = action.splices[j]; - uint256 sourceActionIndex = uint64(spliceInfo); - if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); - - uint256 srcOffset = uint64(spliceInfo >> 64); - uint256 dstOffset = uint64(spliceInfo >> 128); - uint256 length = spliceInfo >> 192; - bytes memory source = results[sourceActionIndex]; - if (srcOffset + length > source.length || dstOffset + length > callData.length) { - revert SpliceOutOfBounds(i, j); - } - - assembly ("memory-safe") { - mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) - } - - unchecked { - ++j; - } - } - - bool success; - uint256 actionInfo = action.actionInfo; - bool storeResult = (actionInfo & 0xff00) != 0; - uint256 callType = actionInfo & 0xff; - address target = address(uint160(actionInfo >> 16)); - - if (callType == uint256(CallType.STATICCALL)) { - assembly ("memory-safe") { - success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) - } - } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { - if (callData.length < 32) revert MissingNativeValue(i); - uint256 callValue; - uint256 payloadLength = callData.length - 32; - assembly ("memory-safe") { - callValue := mload(add(callData, 0x20)) - success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) - } - } else { - assembly ("memory-safe") { - success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) - } - } - - if (!success || storeResult) { - bytes memory ret; - assembly ("memory-safe") { - let returnDataSize := returndatasize() - ret := mload(0x40) - mstore(ret, returnDataSize) - returndatacopy(add(ret, 0x20), 0, returnDataSize) - mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) - } - if (!success) revert CallFailed(i, ret); - results[i] = ret; - } - unchecked { - ++i; - } - } - } - - // ========================================================================= - // Internal: simple call dispatcher (used by monolithic path) - // ========================================================================= - - function _doCall(address target, uint256 value, bytes memory data, bool storeResult) - internal - returns (bytes memory ret) - { - bool success; - assembly ("memory-safe") { - success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) - } - - if (!success || storeResult) { - assembly ("memory-safe") { - let returnDataSize := returndatasize() - ret := mload(0x40) - mstore(ret, returnDataSize) - returndatacopy(add(ret, 0x20), 0, returnDataSize) - mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) - } - if (!success) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) - } - } - } - } - - function _decodeReturnWord(bytes memory ret, uint256 wordOffset) internal pure returns (uint256 word) { - uint256 offset = wordOffset * 32; - if (offset + 32 > ret.length) revert ReturnDataOutOfBounds(); - - assembly ("memory-safe") { - word := mload(add(add(ret, 0x20), offset)) - } - } -} diff --git a/src/minimal/BungeeOpenRouterMinimal.sol b/src/minimal/BungeeOpenRouterMinimal.sol deleted file mode 100644 index 78aad53..0000000 --- a/src/minimal/BungeeOpenRouterMinimal.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; - -/// @title BungeeOpenRouterMinimal (v3, modular w/o splicing) -/// @notice Smallest possible signed-action runner. Identical surface to -/// `BungeeOpenRouterModular` minus the splice mechanism: each -/// `Action` is dispatched standalone via `CALL`, `DELEGATECALL`, or -/// `STATICCALL`, and there is no plumbing of returndata into the -/// next action's calldata. -/// -/// This relies on the assumption that whenever a step needs the -/// "real" amount produced by a previous step (typical for swap-then- -/// bridge flows), the next step's target can re-read that amount -/// itself - usually by calling `balanceOf(this)` at runtime, which -/// is exactly what `BaseRouterSingleOutput`-style pre/post balance -/// deltas do already. -/// -/// @dev Same signing scheme as the other variants: personal_sign over -/// keccak256(abi.encode(chainid, this, exec)). Caller cannot reorder -/// or retarget actions; only re-submission patterns are restricted. -contract BungeeOpenRouterMinimal is OpenRouterAuthBase { - enum CallType { - CALL, - DELEGATECALL, - STATICCALL - } - - struct Action { - CallType callType; - address target; - uint256 value; // forwarded ETH; must be zero for non-CALL types - bytes data; - } - - struct Execution { - Action[] actions; - uint256 nonce; - uint256 deadline; - } - - error ValueOnNonCall(); - error EmptyExecution(); - error UnknownCallType(); - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - function performExecution(Execution calldata exec, bytes calldata signature) external payable virtual { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } - - /// @notice Internal action loop, exposed to subclasses. - function _performActions(Action[] calldata actions) internal { - uint256 actionsLen = actions.length; - if (actionsLen == 0) { - revert EmptyExecution(); - } - - for (uint256 i = 0; i < actionsLen;) { - Action calldata a = actions[i]; - _performAction(a.callType, a.target, a.value, a.data); - unchecked { - ++i; - } - } - } - - /// @notice Dispatches a single action; bubbles any revert. - function _performAction(CallType callType, address target, uint256 value, bytes memory data) internal virtual { - bool ok; - bytes memory ret; - if (callType == CallType.CALL) { - (ok, ret) = target.call{value: value}(data); - } else if (callType == CallType.DELEGATECALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.delegatecall(data); - } else if (callType == CallType.STATICCALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.staticcall(data); - } else { - revert UnknownCallType(); - } - - if (!ok) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) - } - } - } -} diff --git a/src/minimal/BungeeOpenRouterMinimalAH.sol b/src/minimal/BungeeOpenRouterMinimalAH.sol deleted file mode 100644 index f62b112..0000000 --- a/src/minimal/BungeeOpenRouterMinimalAH.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {BungeeOpenRouterMinimal} from "./BungeeOpenRouterMinimal.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; - -/// @title BungeeOpenRouterMinimalAH -/// @notice AllowanceHolder variant of `BungeeOpenRouterMinimal`. Adds the -/// confused-deputy `balanceOf` shim and a user-bound entrypoint that -/// pins the signed payload to a specific `signedUser` (the AH.exec -/// caller). Apart from that, the action loop is identical to v3. -contract BungeeOpenRouterMinimalAH is BungeeOpenRouterMinimal, AllowanceHolderContext { - error CallerNotSignedUser(); - - constructor(address _owner, address _openRouterSigner) - BungeeOpenRouterMinimal(_owner, _openRouterSigner) - {} - - /// @notice AllowanceHolder-aware entrypoint. Same role as - /// `BungeeOpenRouterModularAH.performExecutionAH` - prevents a - /// signed payload meant for user A from being submitted via user - /// B's AllowanceHolder.exec to grief user A's nonce. - function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) external payable { - if (_msgSender() != signedUser) { - revert CallerNotSignedUser(); - } - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), signedUser, exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } -} diff --git a/src/modular/BungeeOpenRouterModular.sol b/src/modular/BungeeOpenRouterModular.sol deleted file mode 100644 index 806a538..0000000 --- a/src/modular/BungeeOpenRouterModular.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; -import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; - -/// @title BungeeOpenRouterModular (v2, modular + returndata splicing) -/// @notice Lightweight, generic open-router. Only signature verification is -/// hard-wired into the contract; every other step (token pull, pre- -/// swap fee, swap, post-swap fee, bridge call) is just an `Action` -/// executed via `CALL`, `DELEGATECALL`, or `STATICCALL`. -/// -/// To plumb the *output of a previous step into the input calldata -/// of the next*, each `Action` carries a list of `Splice`s. Each -/// splice copies a slice of the previous action's returndata into a -/// specific byte offset of this action's calldata. This generalises -/// the single-position `mstore` patching used in `GenericStakedRoute` -/// and `BungeeApproveAndBridge` to multiple positions of any length. -/// -/// @dev The base calldata for every action comes from the caller (and is -/// therefore covered by the signature). Splices only mutate parts of -/// that base calldata - they cannot replace it wholesale, so even if -/// one of the actions returns adversarial bytes, an attacker can only -/// move signed amount-shaped data, not redirect the call target or -/// alter unrelated fields. -contract BungeeOpenRouterModular is OpenRouterAuthBase { - enum CallType { - CALL, - DELEGATECALL, - STATICCALL - } - - /// @notice Describes a single byte-range copy from the previous action's - /// returndata into this action's calldata. - struct Splice { - uint256 srcOffset; // offset within the previous returndata - uint256 dstOffset; // offset within this action's `data` - uint256 length; // number of bytes to copy - } - - /// @notice One step in the execution pipeline. - struct Action { - CallType callType; - address target; - uint256 value; // forwarded ETH; must be zero for non-CALL types - bytes data; // mutable in memory: splices may patch parts of it - Splice[] splices; // applied BEFORE this action runs - } - - struct Execution { - Action[] actions; - uint256 nonce; - uint256 deadline; - } - - error ValueOnNonCall(); - error EmptyExecution(); - error UnknownCallType(); - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - /// @notice Executes a signed sequence of actions. - /// @dev The signed digest binds chainId, this contract, and the entire - /// action set, so the caller cannot reorder, retarget, or strip - /// splices from any action. - function performExecution(Execution calldata exec, bytes calldata signature) external payable virtual { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } - - /// @notice Internal executor for the action loop. Split out so variants - /// (e.g. the AllowanceHolder variant) can add bindings on top of - /// the base signature check without duplicating the loop. - function _performActions(Action[] calldata actions) internal { - uint256 actionsLen = actions.length; - if (actionsLen == 0) { - revert EmptyExecution(); - } - - bytes memory prevReturn; // empty for the first action; splices on action 0 are illegal - for (uint256 i = 0; i < actionsLen;) { - Action calldata a = actions[i]; - - // Copy the action's data into memory so we can splice it in-place. - bytes memory data = a.data; - - // Apply splices: copy slices from prevReturn into data. - uint256 spLen = a.splices.length; - for (uint256 j = 0; j < spLen;) { - Splice calldata sp = a.splices[j]; - BytesSpliceLib.spliceBytes({ - dst: data, // this action's calldata (base is signed; patched before dispatch) - dstOffset: sp.dstOffset, // write `length` bytes into `dst` starting here - src: prevReturn, // read from the previous action's returndata - srcOffset: sp.srcOffset, // copy slice starting at this offset in `src` - length: sp.length // number of bytes to copy (overwrites same span in `dst`) - }); - unchecked { - ++j; - } - } - - prevReturn = _performAction(a.callType, a.target, a.value, data); - - unchecked { - ++i; - } - } - } - - /// @notice Dispatches a single action and returns its returndata. Reverts - /// are bubbled with the underlying revert data. - function _performAction(CallType callType, address target, uint256 value, bytes memory data) - internal - virtual - returns (bytes memory ret) - { - bool ok; - if (callType == CallType.CALL) { - (ok, ret) = target.call{value: value}(data); - } else if (callType == CallType.DELEGATECALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.delegatecall(data); - } else if (callType == CallType.STATICCALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.staticcall(data); - } else { - revert UnknownCallType(); - } - - if (!ok) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) - } - } - } -} diff --git a/src/modular/BungeeOpenRouterModularAH.sol b/src/modular/BungeeOpenRouterModularAH.sol deleted file mode 100644 index 968aa68..0000000 --- a/src/modular/BungeeOpenRouterModularAH.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {BungeeOpenRouterModular} from "./BungeeOpenRouterModular.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; - -/// @title BungeeOpenRouterModularAH -/// @notice AllowanceHolder variant of `BungeeOpenRouterModular`. The actual -/// AllowanceHolder pull is just one of the modular `Action`s (a -/// `CALL` to `ALLOWANCE_HOLDER` with `transferFrom(token, user, this, -/// amount)` calldata), so this contract adds very little on top of -/// the base modular contract: -/// -/// - `AllowanceHolderContext` for the dummy `balanceOf` shim that -/// passes AllowanceHolder's confused-deputy probe. -/// - A new `performExecutionAH` entrypoint that takes an explicit -/// `signedUser` argument, includes it in the signed digest, and -/// enforces `_msgSender() == signedUser`. This stops a malicious -/// actor from wrapping someone else's signed payload inside their -/// own `AllowanceHolder.exec` to grief their nonce. -/// -/// @dev Even without the explicit `signedUser` check the AllowanceHolder -/// allowance scoping (`operator + owner + token`) prevents actual -/// fund theft - any pull whose `owner` differs from the AH.exec -/// caller will revert. The `signedUser` binding is purely to avoid -/// someone else burning a signed-but-unsubmitted payload. -contract BungeeOpenRouterModularAH is BungeeOpenRouterModular, AllowanceHolderContext { - error CallerNotSignedUser(); - - constructor(address _owner, address _openRouterSigner) - BungeeOpenRouterModular(_owner, _openRouterSigner) - {} - - /// @notice AllowanceHolder-aware entrypoint. Bind the signed payload to a - /// specific user so it can only be submitted via that user's - /// `AllowanceHolder.exec` call. - function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) external payable { - if (_msgSender() != signedUser) { - revert CallerNotSignedUser(); - } - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), signedUser, exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } -} diff --git a/src/monolithic/BungeeOpenRouter.sol b/src/monolithic/BungeeOpenRouter.sol deleted file mode 100644 index 4ed46bf..0000000 --- a/src/monolithic/BungeeOpenRouter.sol +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; -import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; -import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; - -/// @title BungeeOpenRouter (v1, monolithic) -/// @notice Monolithic, opinionated open-router: pulls ERC20 funds from a user -/// via standard ERC20 `transferFrom`, optionally takes a pre-swap fee, -/// optionally performs a swap, optionally takes a post-swap fee, then -/// executes a single arbitrary bridge call where the final amount is -/// spliced into the bridge calldata at a list of byte positions. -/// -/// This version is the easiest to reason about because every step is -/// laid out explicitly. The trade-off is rigidity - if a route needs -/// a different ordering or a multi-call bridge interaction, see the -/// modular variants (`BungeeOpenRouterModular`, `BungeeOpenRouterMinimal`). -/// -/// @dev Authentication is matched to `Solver` / `StakedRouterReceiver`: -/// - personal_sign + ecrecover via `AuthenticationLib` -/// - single-use nonces marked with the same assembly pattern -/// - signed digest binds `block.chainid` and `address(this)` so that a -/// payload meant for one deployment cannot be replayed elsewhere. -/// - the user, input token + amount, both fee transfers, the swap, -/// and the bridge calldata are ALL part of the signed payload, so a -/// malicious caller cannot redirect funds. -contract BungeeOpenRouter is OpenRouterAuthBase { - // marked virtual so AllowanceHolder variants can override the pull step - // without duplicating the rest of the body. - using SafeTransferLib for address; - - /// @notice Who is sending funds and how much. - struct InputData { - address user; - address inputToken; - uint256 inputAmount; - } - - /// @notice Optional fee taken in the input token before a swap, or in the - /// bridge token when there is no swap. Set `receiver` to address(0) - /// and `amount` to 0 to skip. - struct FeeData { - address receiver; - uint256 amount; - } - - /// @notice Optional swap step. Set `target` to address(0) to skip entirely. - struct SwapData { - address target; - address approvalSpender; // 0 to skip ERC20 approval - address outputToken; // token measured for balance delta - uint256 value; // ETH forwarded to the swap target - uint256 minOutput; // minimum balance delta; reverts if not met - bytes data; - } - - /// @notice Mandatory bridge call. `amountPositions` lists every byte offset - /// in `data` where the final amount (post-fees) must be written - /// before dispatching the call. - struct BridgeData { - address target; - address approvalSpender; // 0 to skip ERC20 approval - uint256 value; // ETH forwarded to the bridge target - bytes data; - uint256[] amountPositions; - } - - /// @notice Full signed payload for one execution. - /// @dev Signed via personal_sign over keccak256(abi.encode(chainid, this, exec)). - struct Execution { - InputData input; - FeeData preFee; // taken in inputToken before swap - SwapData swap; - FeeData postFee; // taken in finalToken after swap - BridgeData bridge; - uint256 nonce; - uint256 deadline; - } - - error SwapOutputInsufficient(); - error InsufficientFunds(); - error InvalidExecution(); - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - /// @notice Executes the signed payload end-to-end. - /// @dev Anyone can call this; the security boundary is the signature. - function performExecution(Execution calldata exec, bytes calldata signature) external payable { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - - if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { - revert InvalidExecution(); - } - - // 1. pull funds from user; ERC20 transferFrom on the base contract, - // AllowanceHolder transferFrom on the AH variant. - _pullFromUser(exec.input.inputToken, exec.input.user, exec.input.inputAmount); - - // 2. optional pre-swap fee in input token - if (exec.preFee.amount != 0) { - CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); - } - - // 3. optional swap, accounted via balance delta - address finalToken; - uint256 finalAmount; - if (exec.swap.target != address(0)) { - (finalToken, finalAmount) = _performSwap(exec); - } else { - // no swap path: input minus pre-fee is what we have on-hand - if (exec.preFee.amount > exec.input.inputAmount) { - revert InsufficientFunds(); - } - finalToken = exec.input.inputToken; - unchecked { - finalAmount = exec.input.inputAmount - exec.preFee.amount; - } - } - - // 4. optional post-swap fee in final token - if (exec.postFee.amount != 0) { - if (exec.postFee.amount > finalAmount) { - revert InsufficientFunds(); - } - CurrencyLib.transfer(finalToken, exec.postFee.receiver, exec.postFee.amount); - unchecked { - finalAmount -= exec.postFee.amount; - } - } - - // 5. patch bridge calldata with final amount at every signed position - bytes memory bridgeData = exec.bridge.data; - BytesSpliceLib.spliceWords({data: bridgeData, positions: exec.bridge.amountPositions, word: finalAmount}); - - // 6. optional approval to the bridge spender (no-op if same as target via permit / native) - if (exec.bridge.approvalSpender != address(0) && finalToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(finalToken, exec.bridge.approvalSpender, finalAmount); - } - - // 7. dispatch the bridge call, bubbling any revert - _performAction(exec.bridge.target, exec.bridge.value, bridgeData); - } - - /// @notice Hook for pulling `amount` of `token` from `user` into this - /// contract. Default uses ERC20 transferFrom; the AllowanceHolder - /// variant overrides this to call AllowanceHolder. - function _pullFromUser(address token, address user, uint256 amount) internal virtual { - SafeTransferLib.safeTransferFrom(token, user, address(this), amount); - } - - /// @dev Split out so the main `performExecution` body stays under the - /// marketplace "≤ 100 lines / SRP" guideline. - function _performSwap(Execution calldata exec) internal returns (address finalToken, uint256 finalAmount) { - // Snapshot pre-swap balance of the swap output token on this contract. - uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - - // Approve swap router to pull the input token if it expects an allowance. - if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - // amount available for swap = inputAmount - preFee - uint256 swapInput; - unchecked { - swapInput = exec.input.inputAmount - exec.preFee.amount; - } - SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); - } - - _performAction(exec.swap.target, exec.swap.value, exec.swap.data); - - uint256 postBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - if (postBalance < preBalance) { - revert SwapOutputInsufficient(); - } - uint256 delta; - unchecked { - delta = postBalance - preBalance; - } - if (delta < exec.swap.minOutput) { - revert SwapOutputInsufficient(); - } - - finalToken = exec.swap.outputToken; - finalAmount = delta; - } -} diff --git a/src/monolithic/BungeeOpenRouterAH.sol b/src/monolithic/BungeeOpenRouterAH.sol deleted file mode 100644 index 6a1805e..0000000 --- a/src/monolithic/BungeeOpenRouterAH.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.34; - -import {BungeeOpenRouter} from "./BungeeOpenRouter.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; -import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; - -/// @title BungeeOpenRouterAH -/// @notice AllowanceHolder variant of `BungeeOpenRouter`. Identical flow, -/// except that user funds are pulled via 0x's AllowanceHolder -/// (transient-storage allowance) rather than a persistent ERC20 -/// allowance to this contract. -/// -/// Expected flow: -/// 1. user (off-chain) approves AllowanceHolder for `inputToken`. -/// 2. backend signer signs the same `Execution` payload as v1. -/// 3. user calls `AllowanceHolder.exec(operator=this, inputToken, -/// inputAmount, target=this, callData=this.execute(...))`. -/// 4. AllowanceHolder writes a transient allowance and forwards the -/// call to this contract with the user's address appended to -/// calldata (ERC-2771 style). -/// 5. this contract verifies the signature, then calls -/// `AllowanceHolder.transferFrom(inputToken, user, address(this), -/// inputAmount)` to pull the funds. -/// 6. remaining steps are identical to v1. -/// -/// @dev We enforce `_msgSender() == exec.user` so the AllowanceHolder -/// ephemeral allowance (keyed by `operator + owner + token`) actually -/// belongs to the user named in the signed payload. -contract BungeeOpenRouterAH is BungeeOpenRouter, AllowanceHolderContext { - error CallerNotSignedUser(); - - constructor(address _owner, address _openRouterSigner) BungeeOpenRouter(_owner, _openRouterSigner) {} - - /// @notice Override the v1 fund-pull hook to use AllowanceHolder. - /// @dev Assembly path mirrors `0x-settler/src/core/Permit2Payment.sol` - /// `_allowanceHolderTransferFrom`. AllowanceHolder's `transferFrom` - /// either reverts or returns true, so we don't bother decoding the - /// return value. - function _pullFromUser(address token, address user, uint256 amount) internal override { - // The signed user MUST equal the original AllowanceHolder.exec caller, - // because AllowanceHolder writes the transient allowance for - // (operator=this, owner=msg.sender_to_AH, token). - if (_msgSender() != user) { - revert CallerNotSignedUser(); - } - - address allowanceHolder = address(ALLOWANCE_HOLDER); - // Build calldata for: AllowanceHolder.transferFrom(token, user, address(this), amount) - // Selector: 0x15dacbea = bytes4(keccak256("transferFrom(address,address,address,uint256)")) - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(add(0x80, ptr), amount) - mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding - // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which - // shifts the 20-byte address out of place and corrupts the calldata token. Same as - // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. - mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) - mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding - - if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { - let p := mload(0x40) - returndatacopy(p, 0x00, returndatasize()) - revert(p, returndatasize()) - } - } - } -} From b3cf26884cccebe8b52f7bdd42001cc8fbd0cc97 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 18:29:45 +0530 Subject: [PATCH 54/69] refactor: comments --- src/BungeeOpenRouter.sol | 166 +++++++++++++++++++++++---------------- 1 file changed, 97 insertions(+), 69 deletions(-) diff --git a/src/BungeeOpenRouter.sol b/src/BungeeOpenRouter.sol index 60abf98..b4d171c 100644 --- a/src/BungeeOpenRouter.sol +++ b/src/BungeeOpenRouter.sol @@ -49,10 +49,6 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { uint256 value; } - // ========================================================================= - // Modular execution types - // ========================================================================= - enum CallType { CALL, STATICCALL, @@ -81,7 +77,7 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { // bit 1 : BALANCE_FLAG_BIT_MASK (0x02) — swap output: returndata vs balance delta // bit 0 : POST_FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap // - // Combined values for swap()/swapAndBridge(): + // Combined values for flags: // // flags binary (low byte) postFee? balance-of output? bridge value? // ───── ────────────────── ──────── ────────────────── ───────────── @@ -91,21 +87,21 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { // 0x03 00000011 yes balance delta on outputToken bridge.value // 0x04 00000100 no returndata word finalAmount + bridge.value // - // POST_FEE_FLAG_BIT_MASK selects bit 0 — fee timing. - // Cleared — pull → deduct fee from input token → swap remainder. - // Set — pull → swap full input → deduct fee from output token (after minOutput check on swap result). + // POST_FEE_FLAG_BIT_MASK selects bit 0 — fee timing + // 0000 — pre-swap fee: pull → deduct fee from input token → swap remainder + // 0001 — post-swap fee: pull → swap full input → deduct fee from output token (after minOutput check on swap result) // - // BALANCE_FLAG_BIT_MASK selects bit 1 — swap output sizing. - // Cleared — decode returned amount from call returndata at `swapData.returnDataWordOffset`. - // Set — snapshot outputToken balance before call, measure (after − before) as output. + // BALANCE_FLAG_BIT_MASK selects bit 1 — swap output sizing + // 0000 — returnData as swap output: decode returned amount from call returndata at `swapData.returnDataWordOffset` + // 0010 — balanceOf() delta as swap output: snapshot outputToken balance before call, measure (after − before) as output // - // BRIDGE_VALUE_FLAG_BIT_MASK selects bit 2 — bridge native value source. - // Cleared — forward `bridge.value` as msg.value. - // Set — forward `finalAmount + bridge.value` as msg.value (bridge.value carries static addend, e.g. LZ nativeFee). + // BRIDGE_VALUE_FLAG_BIT_MASK selects bit 2 — bridge native value source + // 0000 — bridge.value as msg.value: forward `bridge.value` as msg.value + // 0100 — finalAmount + bridge.value as msg.value: forward `finalAmount + bridge.value` as msg.value (bridge.value carries static addend, e.g. LZ nativeFee) // // BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK selects bit 3 — bridge calldata amount splicing. - // Cleared — no runtime amount splice. - // Set — splice finalAmount at uint16(flags >> BRIDGE_AMOUNT_POSITION_SHIFT). + // 0000 — no bridge calldata modification + // 1000 — bridge calldata modification: splice finalAmount at uint16(flags >> BRIDGE_AMOUNT_POSITION_SHIFT) // /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. @@ -150,8 +146,10 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { // Constructor // ========================================================================= - /// @notice Deploys the router and grants `RESCUE_ROLE` to `_owner`. - /// @param _owner Initial contract owner and rescue-role holder. + /** + * @notice Deploys the router and grants `RESCUE_ROLE` to `_owner`. + * @param _owner Initial contract owner and rescue-role holder. + */ constructor(address _owner) AccessControl(_owner) { _grantRole(RESCUE_ROLE, _owner); } @@ -164,21 +162,20 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { // ========================================================================= /** - * @notice Pull → optional pre/post fee → swap. + * @notice Perform swap with optional pre/post fee. * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param flags Packed flags * @param input User, input token, and pull amount. - * @param receiver Address that ultimately receives the swap output (net of any post-swap fee). For pre-fee / no-fee: the swap router must - * be instructed (via `swapCallData`) to send tokens directly to `receiver`; the contract never holds the output. For post-fee: tokens land - * at this contract, fee is deducted, net is forwarded to `receiver`. - * @param flags Packed flags; OR masks then test with `flags & MASK != 0`. Masks: `POST_FEE_FLAG_BIT_MASK` (0x01), `BALANCE_FLAG_BIT_MASK` - * (0x02). Common values: `0` (pre-fee, returndata), `1` (post-fee, returndata), `2` (pre-fee, balance delta), `3` (post-fee, balance delta). - * @param fee Set `amount` to 0 to skip fee collection. + * @dev For pre-fee / no-fee: the swap router must + * be instructed (via `swapCallData`) to send tokens directly to `receiver`; the contract never holds the output. + * For post-fee: tokens land at this contract, fee is deducted, net is forwarded to `receiver`. + * @param fee Fee collection info: receiver and amount. Set `amount` to 0 to skip fee collection. * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. * @param swapCallData Calldata forwarded to `swapData.target`. - * @return finalAmount Gross swap output before any post-swap fee; net delivered to `receiver` on post-fee paths. - * @dev `minOutput` is the minimum gross amount coming out of the swap (before any output-token fee). It is enforced immediately after - * `_execSwap`, then post-swap fee (if any) is collected. Pre-fee paths take the input-side fee before the swap; `minOutput` still guards the - * swap outcome. + * @param receiver Address that ultimately receives the swap output (net of any post-swap fee). + * @return finalAmount Gross swap output sent to receiver after any post-swap fee + * @dev `minOutput` is the minimum gross amount coming out of the swap (before any output-token fee). It is enforced immediately after `_execSwap`, then post-swap fee (if any) is collected. + * Pre-fee paths take the input-side fee before the swap; `minOutput` still guards the swap outcome. */ function swap( bytes32 quoteId, @@ -196,13 +193,15 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { revert InvalidExecution(); } - bool postFee = fee.amount != 0 && flags & POST_FEE_FLAG_BIT_MASK != 0; - bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; + // Parse flags + bool postFee = fee.amount != 0 && ((flags & POST_FEE_FLAG_BIT_MASK) != 0); + bool useBalanceOf = ((flags & BALANCE_FLAG_BIT_MASK) != 0); { + // Pull funds from user via AllowanceHolder _pullFromUser(input.inputToken, input.user, input.inputAmount); - // POST_FEE_FLAG_BIT_MASK: unset ⇒ collect fee from input before swap; set ⇒ swap first, fee from output + // Collect pre-swap fee uint256 swapInput = input.inputAmount; if (fee.amount != 0 && !postFee) { uint256 feeAmount = fee.amount; @@ -212,38 +211,43 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } } + // Approve swap spender if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); } } - // Post-fee: swap output lands at this contract so the fee can be deducted before forwarding. - // Pre-fee / no-fee: swap calldata encodes `receiver` as the output recipient; tokens never touch this contract. + /// @dev Pre-fee / no-fee: swap calldata encodes `receiver` as the output recipient; tokens never touch this contract. + /// @dev Post-fee: swap output lands at this contract so the fee can be deducted before forwarding. address outputReceiver = postFee ? address(this) : receiver; - // BALANCE_FLAG_BIT_MASK: unset ⇒ decode output word from returndata; set ⇒ output = delta on outputToken + // Execute swap finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, outputReceiver); if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); if (postFee) { + // Collect post-swap fee uint256 feeAmount = fee.amount; CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); unchecked { finalAmount -= feeAmount; } + + // Transfer net output to receiver CurrencyLib.transfer(swapData.outputToken, receiver, finalAmount); } + // Pre-fee / no-fee: tokens were sent directly to `receiver` by the swap router; nothing to transfer emit RequestExecuted(quoteId); } /** - * @notice Pull → optional pre/post swap fee → swap → bridge with runtime amount splicing. + * @notice Perform swap and bridge with optional pre/post swap fee. * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param flags Packed flags * @param input User, input token, and pull amount. - * @param flags Same packing as `swap`; bits 2–3 also control bridge `msg.value` and calldata splicing. - * @param fee Set `amount` to 0 to skip fee collection. + * @param fee Fee collection info: receiver and amount. Set `amount` to 0 to skip fee collection. * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. * @param swapCallData Calldata forwarded to `swapData.target`. * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. @@ -267,22 +271,25 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { revert InvalidExecution(); } + // Execute swap before bridge uint256 finalAmount = _swapBeforeBridge(flags, input, fee, swapData, swapCallData); + + // Execute bridge _doBridge(swapData.outputToken, finalAmount, flags, bridgeData, bridgeCallData); + emit RequestExecuted(quoteId); } /** - * @notice Pull → optional pre-bridge fee → bridge, with no swap step. + * @notice Perform bridge with optional pre-bridge fee. * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. * @param input User, input token, and pull amount. - * @param fee Pre-bridge fee taken from the input token; set `amount` to 0 to skip. + * @param fee Fee collection info: receiver and amount. Set `amount` to 0 to skip fee collection. * @param bridgeData Bridge target, approval spender, and `msg.value` for the bridge call. * @param bridgeCallData Calldata forwarded to `bridgeData.target` (amount must be baked in by the caller). - * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is fully knowable by the caller before signing. The caller must - * therefore bake the correct amount directly into `bridgeCallData` and set `bridgeData.value` to the desired `msg.value` for the bridge - * call. No runtime calldata splicing is performed. The caller MUST route through `AllowanceHolder.exec` for ERC-20 inputs so that - * `_msgSender()` resolves to `input.user`. + * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is fully knowable by the caller before signing. + * The caller must therefore bake the correct amount directly into `bridgeCallData` and set `bridgeData.value` to the desired `msg.value` for the bridge call. + * No runtime calldata splicing is performed. The caller MUST route through `AllowanceHolder.exec` for ERC-20 inputs so that `_msgSender()` resolves to `input.user`. */ function bridge( bytes32 quoteId, @@ -295,13 +302,16 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { revert InvalidExecution(); } + // Pull funds from user via AllowanceHolder _pullFromUser(input.inputToken, input.user, input.inputAmount); + // Collect pre-bridge fee uint256 feeAmount = fee.amount; if (feeAmount != 0) { CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); } + // Approve bridge spender if (bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { uint256 netAmount; unchecked { @@ -310,7 +320,9 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, netAmount); } + // Execute bridge _doCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); + emit RequestExecuted(quoteId); } @@ -326,15 +338,13 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { returns (bytes[] memory results) { results = _performActions(actions); + emit RequestExecuted(quoteId); } // ========================================================================= // Internal functions // ========================================================================= - // - // swap / bridge — orchestration is inline in the external functions; they use - // _pullFromUser, _execSwap (swap), and _doCallCalldata (bridge) from common below. // ------------------------------------- // swapAndBridge internal functions @@ -342,8 +352,8 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { /** * @dev Pull, optional pre/post swap fee, and swap for `swapAndBridge`. Swap output always remains at `address(this)` for bridging. - * @param input User, input token, and pull amount. * @param flags Fee timing and swap output measurement flags (same as `swap`). + * @param input User, input token, and pull amount. * @param fee Fee receiver and amount; `amount == 0` skips fee collection. * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. * @param swapCallData Calldata forwarded to `swapData.target`. @@ -356,12 +366,14 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { SwapData calldata swapData, bytes calldata swapCallData ) internal returns (uint256 finalAmount) { + // Pull funds from user via AllowanceHolder _pullFromUser(input.inputToken, input.user, input.inputAmount); + bool postFee; { - // POST_FEE_FLAG_BIT_MASK: same semantics as standalone `swap` (fee from input vs output token) + // Collect pre-swap fee uint256 feeAmount = fee.amount; - postFee = feeAmount != 0 && flags & POST_FEE_FLAG_BIT_MASK != 0; + postFee = feeAmount != 0 && ((flags & POST_FEE_FLAG_BIT_MASK) != 0); uint256 swapInput = input.inputAmount; if (feeAmount != 0 && !postFee) { @@ -371,17 +383,19 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } } + // Approve swap spender if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); } } - // BALANCE_FLAG_BIT_MASK: same returndata vs balance-delta measurement as `swap` - // Swap output always lands at this contract regardless of fee timing — tokens must be here for bridging. - bool useBalanceOf = flags & BALANCE_FLAG_BIT_MASK != 0; + // Execute swap + /// @dev Swap output always lands at this contract regardless of fee timing — tokens must be here for bridging. + bool useBalanceOf = ((flags & BALANCE_FLAG_BIT_MASK) != 0); finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, address(this)); if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + // Collect post-swap fee if (postFee) { uint256 feeAmount = fee.amount; CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); @@ -395,9 +409,9 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { * @dev Splice `amount` into bridge calldata when flagged, approve the bridge spender, and call the bridge target. * @param token ERC-20 bridged (or native sentinel); used for approval only. * @param amount Post-swap token amount spliced into calldata and/or forwarded as `msg.value`. + * @param flags Bridge splice position, `msg.value` composition, and related bit flags. * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. * @param bridgeCallData Base bridge calldata; copied to memory when splicing is required. - * @param flags Bridge splice position, `msg.value` composition, and related bit flags. */ function _doBridge( address token, @@ -407,17 +421,22 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { bytes calldata bridgeCallData ) internal { bytes memory _bridgeCallData = bridgeCallData; + + // Modify bridge calldata if splicing is required if (flags & BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK != 0) { uint256 position = flags >> BRIDGE_AMOUNT_POSITION_SHIFT & BRIDGE_AMOUNT_POSITION_MASK; BytesSpliceLib.spliceWord({data: _bridgeCallData, position: position, word: amount}); } + // Approve bridge spender if (bridgeData.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { SafeTransferLib.safeApproveWithRetry(token, bridgeData.approvalSpender, amount); } - // when set, forward amount as msg.value for native-token bridges - uint256 bridgeValue = flags & BRIDGE_VALUE_FLAG_BIT_MASK != 0 ? amount + bridgeData.value : bridgeData.value; + // Parse and set bridge value flag + uint256 bridgeValue = ((flags & BRIDGE_VALUE_FLAG_BIT_MASK) != 0) ? amount + bridgeData.value : bridgeData.value; + + // Execute bridge call _doCall(bridgeData.target, bridgeValue, _bridgeCallData); } @@ -426,8 +445,12 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { // -------------------------------------- /** - * @dev Executes `actions` in order, applying returndata splices before each call. `actionInfo` layout: bits 0–7 call type (`CallType`), bit 8 - * store returndata, bits 16+ target address. `splices[j]` packs source index, src/dst byte offsets, and length. + * @dev Executes `actions` in order, applying returndata splices before each call. + * @dev actionInfo layout: + * - bits 0–7: call type (`CallType`) + * - bit 8: store returndata + * - bits 16+: target address + * splices[j` packs source index, src/dst byte offsets, and length. * @param actions Ordered list of actions to run. * @return results Stored returndata per action when the store-result bit is set. */ @@ -509,15 +532,16 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { // ------------------------------- /** - * @dev Pulls `amount` of `token` from `user` into this contract. For ERC20: enforces `_msgSender() == user` (caller must have routed through - * `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. AH selector: - * transferFrom(address,address,address,uint256) = 0x15dacbea. For native ETH: ETH must already be present as msg.value; verify sufficient - * value was forwarded. No AH call is needed. + * @dev Pulls `amount` of `token` from `user` into this contract. + * For ERC20: enforces `_msgSender() == user` (caller must have routed through `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. + * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea. + * For native ETH: ETH must already be present as msg.value; verify sufficient value was forwarded. * @param token Input token or `CurrencyLib.NATIVE_TOKEN_ADDRESS`. * @param user Owner whose AllowanceHolder-scoped allowance is consumed. * @param amount Tokens or wei to pull. */ function _pullFromUser(address token, address user, uint256 amount) internal { + // Check input value if native token if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { if (msg.value < amount) { revert InsufficientMsgValue(); @@ -525,10 +549,10 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { return; } - if (_msgSender() != user) { - revert CallerNotSignedUser(); - } + // Check caller is user + if (_msgSender() != user) revert CallerNotSignedUser(); + // Call AllowanceHolder.transferFrom() address allowanceHolder = address(ALLOWANCE_HOLDER); assembly ("memory-safe") { let ptr := mload(0x40) @@ -550,10 +574,11 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } /** - * @dev Executes the swap call and returns the output amount. `useBalanceOf=true`: measure output as (balance after − balance before) at - * `outputReceiver`. `useBalanceOf=false`: decode output from returndata at `swapData.returnDataWordOffset`. `outputReceiver` must be - * `address(this)` when tokens are expected at the contract (post-swap fee path, bridge path) or the end user when the router sends directly - * to them. + * @dev Executes the swap call and returns the output amount. + * `useBalanceOf=true`: measure output as (balance after − balance before) at `outputReceiver`. + * `useBalanceOf=false`: decode output from returndata at `swapData.returnDataWordOffset`. + * `outputReceiver` must be `address(this)` when tokens are expected at the contract (post-swap fee path, bridge path) + * or the end user when the router sends directly to them. * @param swapData Swap target, value, output token, and returndata layout. * @param swapCallData Calldata forwarded to `swapData.target`. * @param useBalanceOf When true, use balance delta instead of returndata decoding. @@ -567,10 +592,12 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { address outputReceiver ) internal returns (uint256 finalAmount) { if (useBalanceOf) { + // Measure output as (balance after − balance before) at `outputReceiver` uint256 before = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver); _doCallCalldata(swapData.target, swapData.value, swapCallData, false); finalAmount = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver) - before; } else { + // Decode output from returndata bytes memory ret = _doCallCalldata(swapData.target, swapData.value, swapCallData, true); finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); } @@ -603,6 +630,7 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { /** * @dev Low-level `call` using calldata copied to memory; optionally captures returndata. + * @dev Helps cheaper external calls avoiding early copy of calldata to memory. * @param target Call recipient. * @param value Wei forwarded with the call. * @param data Calldata slice forwarded to `target`. From 4eb6ab2b04fa521f85b0909e96d34ae9c91124b4 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 18:39:38 +0530 Subject: [PATCH 55/69] refactor: remove performActions return --- src/BungeeOpenRouter.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/BungeeOpenRouter.sol b/src/BungeeOpenRouter.sol index b4d171c..fe38272 100644 --- a/src/BungeeOpenRouter.sol +++ b/src/BungeeOpenRouter.sol @@ -330,14 +330,9 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { * @notice Runs a sequence of generic actions with optional returndata splicing between steps. * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. * @param actions Ordered actions; each may splice bytes from a prior action's returndata into its calldata. - * @return results Per-action returndata when the action's `actionInfo` store-result bit is set. */ - function performActions(bytes32 quoteId, Action[] calldata actions) - external - payable - returns (bytes[] memory results) - { - results = _performActions(actions); + function performActions(bytes32 quoteId, Action[] calldata actions) external payable { + _performActions(actions); emit RequestExecuted(quoteId); } @@ -452,11 +447,10 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { * - bits 16+: target address * splices[j` packs source index, src/dst byte offsets, and length. * @param actions Ordered list of actions to run. - * @return results Stored returndata per action when the store-result bit is set. */ - function _performActions(Action[] calldata actions) internal returns (bytes[] memory results) { + function _performActions(Action[] calldata actions) internal { uint256 actionsLength = actions.length; - results = new bytes[](actionsLength); + bytes[] memory results = new bytes[](actionsLength); for (uint256 i; i < actionsLength;) { Action calldata action = actions[i]; From e7a81b55fa38f5589ff57b831090e1d98faa787f Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 19:08:57 +0530 Subject: [PATCH 56/69] refactor: check and set max approval --- src/BungeeOpenRouter.sol | 60 +++++++++++++++++++++++--------- src/common/interfaces/IERC20.sol | 6 ++++ 2 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 src/common/interfaces/IERC20.sol diff --git a/src/BungeeOpenRouter.sol b/src/BungeeOpenRouter.sol index fe38272..3cc89d2 100644 --- a/src/BungeeOpenRouter.sol +++ b/src/BungeeOpenRouter.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; +import {IERC20} from "./common/interfaces/IERC20.sol"; import {AccessControl} from "./common/utils/AccessControl.sol"; import {AllowanceHolderContext} from "./common/allowance/AllowanceHolderContext.sol"; import {ALLOWANCE_HOLDER} from "./common/interfaces/IAllowanceHolder.sol"; @@ -211,9 +212,15 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } } - // Approve swap spender - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + // Approve spender + if ( + // check spender & token + swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + swapInput > IERC20(input.inputToken).allowance(address(this), swapData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, type(uint256).max); } } @@ -311,13 +318,20 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); } + uint256 netAmount; + unchecked { + netAmount = input.inputAmount - feeAmount; + } + // Approve bridge spender - if (bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - uint256 netAmount; - unchecked { - netAmount = input.inputAmount - feeAmount; - } - SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, netAmount); + if ( + // check spender && token + bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + netAmount > IERC20(input.inputToken).allowance(address(this), bridgeData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, type(uint256).max); } // Execute bridge @@ -379,8 +393,14 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } // Approve swap spender - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + if ( + // check spender & token + swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + swapInput > IERC20(input.inputToken).allowance(address(this), swapData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, type(uint256).max); } } @@ -424,8 +444,14 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } // Approve bridge spender - if (bridgeData.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(token, bridgeData.approvalSpender, amount); + if ( + // check spender & token + bridgeData.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + amount > IERC20(token).allowance(address(this), bridgeData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(token, bridgeData.approvalSpender, type(uint256).max); } // Parse and set bridge value flag @@ -568,10 +594,10 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } /** - * @dev Executes the swap call and returns the output amount. - * `useBalanceOf=true`: measure output as (balance after − balance before) at `outputReceiver`. - * `useBalanceOf=false`: decode output from returndata at `swapData.returnDataWordOffset`. - * `outputReceiver` must be `address(this)` when tokens are expected at the contract (post-swap fee path, bridge path) + * @dev Executes the swap call and returns the output amount. + * `useBalanceOf=true`: measure output as (balance after − balance before) at `outputReceiver`. + * `useBalanceOf=false`: decode output from returndata at `swapData.returnDataWordOffset`. + * `outputReceiver` must be `address(this)` when tokens are expected at the contract (post-swap fee path, bridge path) * or the end user when the router sends directly to them. * @param swapData Swap target, value, output token, and returndata layout. * @param swapCallData Calldata forwarded to `swapData.target`. diff --git a/src/common/interfaces/IERC20.sol b/src/common/interfaces/IERC20.sol new file mode 100644 index 0000000..d4ea45e --- /dev/null +++ b/src/common/interfaces/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +interface IERC20 { + function allowance(address owner, address spender) external view returns (uint256); +} From 5307f1b142223f369f9322a2a0c397dfb887e038 Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 19 May 2026 17:49:08 +0400 Subject: [PATCH 57/69] feat: agent docs --- AGENTS.md | 6 + OPENROUTER_ASSUMPTIONS.md | 248 ++++++++++++++++++++++++++++++++++++++ OPENROUTER_CONTEXT.md | 108 +++++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 AGENTS.md create mode 100644 OPENROUTER_ASSUMPTIONS.md create mode 100644 OPENROUTER_CONTEXT.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f714adb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +# Project Context + +For OpenRouter contract work read files which are relevant for the task. general context - `OPENROUTER_CONTEXT.md` +assumptions - `OPENROUTER_ASSUMPTIONS.md` first. + +Main ship target is `src/combined/BungeeOpenRouterV2Unchecked.sol`. If its ABI changes, update the backend encoders in `bungee-backend/src/modules/dex/utils.ts` and `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts`. diff --git a/OPENROUTER_ASSUMPTIONS.md b/OPENROUTER_ASSUMPTIONS.md new file mode 100644 index 0000000..7ba02a6 --- /dev/null +++ b/OPENROUTER_ASSUMPTIONS.md @@ -0,0 +1,248 @@ +# OpenRouter Assumptions + +Last reviewed: 2026-05-19. + +Scope: `src/combined/BungeeOpenRouterV2Unchecked.sol`. + +This document captures the assumptions that make the unchecked OpenRouter safe to operate. Many of these are business and integration assumptions, not guarantees enforced by the contract. + +## Source Of Truth + +`BungeeOpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone. + +Current checked-in public surface: + +- `swap(...)` +- `swapAndBridge(...)` +- `bridge(...)` +- `performModularExecution(...)` +- `rescueFunds(...)` + +`OPENROUTER_CONTEXT.md` and `scripts/e2e/utils/routerAbi.ts` may mention `performExecution(...)`; verify against the Solidity file before relying on that ABI. + +## Enforcement Classes + +Use this distinction when reviewing any route or integration: + +- On-chain enforced: checked directly by the router. +- Operationally enforced: must be true because frontend, backend, deploy config, or runbooks enforce it. +- Policy assumption: not enforced by code. If it becomes false, the unchecked router can become unsafe. + +## Critical Business Assumptions + +### Router Never Holds Durable Funds + +The router may temporarily hold funds during one transaction, but it should not end routes with meaningful token or native balances. + +Failure mode: `performModularExecution` lets any caller make the router call arbitrary contracts. If the router holds ERC20s, native ETH, bridged refunds, swap dust, rebates, or protocol refunds, a public caller can move or approve those assets through modular actions before owner rescue. + +Operational requirements: + +- Do not use the router as a treasury, escrow, settlement account, refund address, or fee vault. +- Route calldata should send final assets to the user, bridge, or fee recipient in the same transaction. +- Monitor router token/native balances and treat non-zero balances as an incident or stuck-funds condition. +- Owner rescue is an operational recovery tool, not a security boundary. + +### Users Never Directly Approve The Router + +Users must not give persistent ERC20, Permit2, ERC721, ERC1155, or protocol-specific approvals directly to the router. + +Failure mode: if a user directly approves the router, any caller can use `performModularExecution` to make the router call `transferFrom`, `approve`, or equivalent privileged token functions against that user allowance. + +Operational requirements: + +- User ERC20 approvals should go to 0x AllowanceHolder, not OpenRouter. +- UI copy and wallet flows must never ask users to approve OpenRouter directly. +- Monitoring should flag direct allowances from users to the router. +- If a direct approval is discovered, revoke it before treating that user as safe. + +### Router Has No Privileged Role On Other Contracts + +No external contract should treat OpenRouter as a privileged actor unless every public caller is allowed to exercise that privilege. + +Failure mode: if another contract has `onlyRouter`, allowlists the router, grants it minter/burner/pauser/admin/operator/bridge-agent permissions, or keys permissions off `msg.sender == router`, any caller can exercise that role through modular execution. + +Operational requirements: + +- Do not grant OpenRouter roles in bridges, vaults, tokens, staking systems, receivers, relayers, or settlement contracts. +- Do not whitelist OpenRouter in downstream contracts as a trusted caller unless the called operation is safe for arbitrary public callers. +- Review new integrations for hidden trust checks against `msg.sender`. + +### Router Is Not A User-Intent Authority + +The unchecked router does not prove that a route reflects user intent. It only executes calldata. + +Failure mode: a malicious UI or compromised backend can make the user call `AllowanceHolder.exec` with calldata that pays an attacker, charges an arbitrary fee, bridges to a wrong recipient, or approves a malicious spender. + +Operational requirements: + +- The frontend/backend must validate recipients, fee receivers, fee amounts, swap targets, bridge targets, approval spenders, destination chain/domain, bridge min amounts, and refund addresses before presenting a transaction. +- Wallet simulation and transaction review should show the actual route effects where possible. +- `requestHash` is only an event correlation id. It does not enforce uniqueness, replay protection, or user consent. + +## Fund Pull Assumptions + +### ERC20 Inputs Use AllowanceHolder + +ERC20 input safety depends on 0x AllowanceHolder transient allowance scoping plus `_msgSender() == input.user`. + +On-chain enforced: + +- `_pullFromUser` reverts unless `_msgSender() == input.user` for ERC20 inputs. +- When called through AllowanceHolder, `_msgSender()` is decoded from the appended user address. + +Operational assumptions: + +- The user calls `AllowanceHolder.exec(operator, token, amount, target, data)`. +- `operator` is the router. +- `target` is the router. +- `token` and `amount` match the route input. +- The user has a persistent approval to AllowanceHolder, not to the router. + +Failure modes: + +- Direct ERC20 calls to the router fail because `_msgSender()` is not the user. +- Bad AH calldata can still execute a bad route if the user submits it. +- AH protects fund pulling for the route input, but it does not validate swap/bridge semantics. + +### Native Inputs Are Not User-Bound + +Native-token input routes only check that `msg.value >= inputAmount`. + +Failure mode: `input.user` is not authenticated for native routes. Anyone can submit native routes if they provide the ETH. This is usually acceptable because the caller funds the transaction, but downstream analytics must not treat `input.user` as authenticated identity for native paths. + +Operational requirements: + +- Native route attribution should come from transaction signer / AH sender / product context, not only `input.user`. +- Excess `msg.value` is not automatically refunded by the router. + +## Execution Assumptions + +### External Targets Are Trusted Per Route + +The router does not whitelist swap targets, bridge targets, approval spenders, manipulators, receivers, or fee recipients. + +Failure modes: + +- Malicious swap target can consume approved input and return misleading returndata. +- Malicious bridge target can consume approved output or native value. +- Malicious approval spender can use allowance after the route if allowance remains and the router later receives the same token. +- Malicious fee receiver can reject native fee transfers and revert the route. + +Operational requirements: + +- Backend/frontend must maintain target and spender allowlists or equivalent route validation. +- Approval spender should be the minimum necessary protocol spender. +- Prefer route patterns that leave no router balance and no meaningful residual allowance. + +### Swap Output Measurement Matches The Aggregator + +The router supports two output modes: + +- Returndata mode: decode a 32-byte word at `swapData.returnDataWordOffset`. +- Balance-delta mode: measure `balanceOf(outputReceiver)` before and after the swap. + +Failure modes: + +- Returndata mode is unsafe if the target return word is not the actual output amount. +- Balance-delta mode is unsafe if unrelated balance changes occur during the call, or if the token has rebasing/fee-on-transfer behavior that breaks expected deltas. +- In standalone pre-fee/no-fee swaps, the swap calldata must send output directly to `receiver`; the router will not forward output afterward. +- In standalone post-fee swaps and all `swapAndBridge` paths, the swap output must land on the router. + +Operational requirements: + +- Choose output mode per aggregator and route. +- Verify `returnDataWordOffset` against the concrete swap target ABI. +- Verify output recipient encoded in `swapCallData` matches the router mode. +- Treat `minOutput` as gross swap output, not guaranteed net-to-user output after post-fee or bridge fees. + +### Fee Semantics Are Caller-Defined + +The router does not enforce fee policy. + +Assumptions: + +- Pre-fee amounts are denominated in the input token. +- Post-fee amounts are denominated in the output token. +- `fee.receiver` is trusted and product-approved. +- `fee.amount` is within product policy. + +Failure modes: + +- A malicious caller can set an arbitrary fee receiver and amount if the user submits the calldata. +- Post-fee is applied after gross `minOutput` validation, so net user proceeds can be lower than `minOutput`. + +### Bridge Calldata Is Semantically Correct + +The router does not understand bridge-specific fields. + +Assumptions: + +- Destination chain/domain is correct. +- Recipient is correct. +- Refund address is not the router unless intentionally safe. +- Bridge min amount / slippage fields are correct. +- Bridge fee quote and native fee buffer are current enough. +- Token and amount fields in calldata match the route. + +Failure modes: + +- `bridge()` performs no runtime amount splicing; the amount must already be encoded. +- `swapAndBridge()` can splice one 32-byte amount word only. +- The bridge-value flag forwards `finalAmount + bridgeData.value` as native value. It must only be used when the bridge expects the bridged asset itself as native value plus a static fee. +- Excess native fee behavior depends on the bridge target and refund address, not OpenRouter. + +## Modular Execution Assumptions + +`performModularExecution` is the broadest surface. It makes the router a public generic call executor. + +Assumptions: + +- The router has no durable funds. +- No user has directly approved the router. +- No external contract gives the router privileged rights. +- Each action target is safe for the router to call. +- Splice offsets and lengths are generated by trusted tooling. +- Actions that are splice sources store their returndata. + +Failure modes: + +- Any public caller can transfer, approve, or spend assets already held by the router. +- Any public caller can exercise downstream privileges granted to the router. +- `CALL_WITH_NATIVE` can spend native ETH already sitting in the router. +- Invalid `callType` values fall through to normal `CALL`; encoders must emit only known call types. +- Splices are bounds-checked but not semantically validated. A bad splice can write a valid but wrong bridge amount, recipient field, fee field, or payload word. + +## Token Assumptions + +Assumptions: + +- ERC20s follow sane `transfer`, `transferFrom`, `approve`, and `balanceOf` behavior. +- The native token sentinel is exactly `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. +- Tokens do not rebase or charge transfer fees in ways that invalidate route amounts, unless route tooling explicitly accounts for that. +- Approval reset/retry behavior in Solady `safeApproveWithRetry` is acceptable for the token. + +Failure modes: + +- Fee-on-transfer tokens can cause bridge approvals or calldata amounts to exceed actual received balances. +- Rebasing tokens can corrupt balance-delta output measurement. +- Non-standard tokens can revert, return false, or have allowance quirks. + +## Operational Checklist + +Before enabling a route or integration, confirm: + +- Users approve AllowanceHolder only. +- The router has no direct user allowances. +- The router has no privileged roles on any touched contract. +- The router is not used as recipient, refund address, treasury, or settlement vault unless public draining is acceptable. +- Swap target, bridge target, approval spenders, manipulators, fee receiver, and receiver are validated. +- Swap output mode and `returnDataWordOffset` are correct for the aggregator. +- Standalone swap recipient is correct for pre-fee/no-fee versus post-fee mode. +- Bridge calldata encodes the correct recipient, destination, min amount, refund address, and fees. +- Bridge amount splice offset is correct for the exact calldata shape. +- Native `msg.value` covers input amount plus all downstream native call values. +- Excess native value and bridge refunds do not end up on the router. +- Monitoring exists for router balances, direct allowances to router, and unexpected downstream roles. + +If any critical business assumption is false, do not rely on `BungeeOpenRouterV2Unchecked` as-is. Add access control, use a signed variant, or remove the downstream privilege/funds/allowance that makes the public call surface dangerous. diff --git a/OPENROUTER_CONTEXT.md b/OPENROUTER_CONTEXT.md new file mode 100644 index 0000000..94a4248 --- /dev/null +++ b/OPENROUTER_CONTEXT.md @@ -0,0 +1,108 @@ +# OpenRouter Contract Context + +Last researched: 2026-05-18. + +Main ship target: + +- `src/combined/BungeeOpenRouterV2Unchecked.sol` + +Use `src/combined/BungeeOpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI. + +## V2Unchecked Surface + +`BungeeOpenRouterV2Unchecked` removes backend signature verification, nonce, and deadline fields. Fund safety for ERC20 inputs depends on 0x AllowanceHolder transient approvals plus `_msgSender() == input.user` in `_pullFromUser`. + +External entrypoints: + +- `performExecution(bytes32 requestHash, MonolithicExecution exec, bytes swapCallData, bytes bridgeCallData)` + - Pulls via AllowanceHolder. + - Optional pre-fee, optional swap, optional post-fee. + - Bridges with optional single amount-word splice controlled by flags. + - Bit 0 fee flag is ignored here; fee placement comes from `preFee` and `postFee`. +- `swap(bytes32 requestHash, InputData input, address receiver, uint256 flags, FeeData fee, SwapData swapData, bytes swapCallData)` + - Same-chain DEX path. + - Pre-fee/no-fee swaps can send output directly to `receiver`. + - Post-fee swaps send output to the router, then the router skims fee and forwards net. +- `swapAndBridge(bytes32 requestHash, InputData input, uint256 flags, FeeData fee, SwapData swapData, bytes swapCallData, BridgeData bridgeData, bytes bridgeCallData)` + - Swap output always lands on the router so it can be bridged. + - Supports runtime bridge amount splice and native bridge-value mode via flags. +- `bridge(bytes32 requestHash, InputData input, FeeData fee, BridgeData bridgeData, bytes bridgeCallData)` + - Direct bridge, no swap. + - No runtime splice; bridge amount must already be encoded in `bridgeCallData`. +- `performModularExecution(bytes32 requestHash, Action[] actions)` + - Generic action loop with packed action metadata and packed splices. + +## Flags + +Flag constants in `BungeeOpenRouterV2Unchecked.sol`: + +- `0x01` - post-swap fee for `swap` and `swapAndBridge`; clear means pre-fee from input. Ignored by `performExecution`. +- `0x02` - measure swap output by `balanceOf` delta; clear means decode return word at `SwapData.returnDataWordOffset`. +- `0x04` - bridge `msg.value = finalAmount + BridgeData.value`; used for native bridge assets. +- `0x08` - splice `finalAmount` into `bridgeCallData`. +- Bits `16..31` - byte offset for the bridge amount splice when `0x08` is set. + +Backend constants live in both: + +- `bungee-backend/src/modules/dex/dex.config.ts` +- `bungee-backend/src/modules/router/router.config.ts` + +Keep those masks and deployed addresses in sync with this contract. + +## Modular Packing + +`Action.actionInfo` is packed as: + +```text +uint8(callType) | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16) +``` + +`Action.splices[]` entries are packed as: + +```text +sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) +``` + +`CallType.CALL_WITH_NATIVE` treats the first 32 bytes of `action.data` as the call value and the remaining bytes as calldata. PoCs use this for native fee transfers and Stargate native sends. + +## Current PoCs + +- `test/poc/OpenOceanAcrossOpenRouterPoC.t.sol` + - Modular OpenOcean USDC -> WETH swap. + - `AcrossERC20AmountManipulator` derives the Across output amount. + - Splices swap output and derived output into `SpokePool.deposit`. +- `test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol` + - Modular OpenOcean USDC -> native ETH. + - `MathManipulator` derives fee, post-fee amount, and bridge amount. + - Uses `CALL_WITH_NATIVE` and splices Stargate `amountLD`. +- `test/poc/OneInchCctpOpenRouterPoC.t.sol` + - CCTP-oriented PoC. + +Fork tests need RPC env vars and sometimes block pins. Example: + +```bash +ARBITRUM_RPC=... ARBITRUM_FORK_BLOCK=461716058 forge test --match-path test/poc/OpenOceanAcrossOpenRouterPoC.t.sol -vv +``` + +## Backend ABI Expectations + +The backend encodes the unchecked ABI in: + +- `bungee-backend/src/modules/dex/utils.ts` + - `swap(...)` + - `AllowanceHolder.exec(...)` +- `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts` + - `bridge(...)` + - `swapAndBridge(...)` + - `AllowanceHolder.exec(...)` + +If the Solidity ABI changes, update those hard-coded ABI strings first. Direct DEX and direct bridge quote builders depend on them. + +## Gotchas + +- ERC20 inputs must be submitted through 0x AllowanceHolder, not directly to the router, or `_msgSender() == user` fails. +- Native input paths send ETH with the outer `AllowanceHolder.exec` call; no ERC20 pull happens. +- `bridge()` cannot splice runtime amounts. Use `swapAndBridge()` when bridge calldata needs the live swap output. +- `swapAndBridge()` uses balance-delta output measurement in backend builders today. +- `performExecution` and `swapAndBridge` share helpers but have different fee semantics. +- Production use of `BungeeOpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks. From 8c3f4f89c63a8e460af05b46cedb90b77ebbca0e Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 20:07:31 +0530 Subject: [PATCH 58/69] fix: tests --- hardhat.config.ts | 2 +- package.json | 1 + scripts/deploy/deployBungeeOpenRouter.ts | 52 + scripts/deploy/deployBungeeOpenRouterV2.ts | 83 -- scripts/e2e/approveViaModular.ts | 21 +- .../e2e/arbitrum/performExecution.postFee.ts | 72 +- .../e2e/arbitrum/performExecution.preFee.ts | 50 +- .../performModularExecution.postFee.ts | 8 +- .../performModularExecution.preFee.ts | 12 +- scripts/e2e/bridgeViaRelay.ts | 492 -------- scripts/e2e/bridgeViaRelaySimple.ts | 234 ---- scripts/e2e/cctp/bridge.preFee.ts | 2 +- scripts/e2e/cctp/performExecution.postFee.ts | 46 +- scripts/e2e/cctp/performExecution.preFee.ts | 59 +- .../cctp/performModularExecution.postFee.ts | 8 +- .../cctp/performModularExecution.preFee.ts | 8 +- .../cctp/swapAndBridge.postFee.balanceOf.ts | 8 +- ...pAndBridge.postFee.returndata.kyberswap.ts | 8 +- .../cctp/swapAndBridge.postFee.returndata.ts | 8 +- .../cctp/swapAndBridge.preFee.balanceOf.ts | 8 +- .../cctp/swapAndBridge.preFee.returndata.ts | 8 +- scripts/e2e/config.ts | 2 +- .../{ => misc}/routerUsdc.withdraw.modular.ts | 24 +- scripts/e2e/oft/bridge.preFee.ts | 2 +- scripts/e2e/oft/performExecution.postFee.ts | 221 ++-- scripts/e2e/oft/performExecution.preFee.ts | 127 +- .../oft/performModularExecution.postFee.ts | 8 +- .../e2e/oft/performModularExecution.preFee.ts | 8 +- .../oft/swapAndBridge.postFee.balanceOf.ts | 8 +- .../oft/swapAndBridge.postFee.returndata.ts | 8 +- .../e2e/oft/swapAndBridge.preFee.balanceOf.ts | 8 +- .../oft/swapAndBridge.preFee.returndata.ts | 8 +- scripts/e2e/relay/aave.bridge.preFee.ts | 2 +- .../e2e/relay/aave.performExecution.preFee.ts | 34 +- .../aave.performModularExecution.preFee.ts | 10 +- scripts/e2e/relay/usdc.bridge.preFee.ts | 2 +- .../e2e/relay/usdc.performExecution.preFee.ts | 34 +- .../usdc.performModularExecution.preFee.ts | 10 +- ...arbUsdcBaseEth.performExecution.postFee.ts | 39 +- ...BaseEth.performModularExecution.postFee.ts | 8 +- ...baseUsdcArbEth.performExecution.postFee.ts | 224 ++-- ...cArbEth.performModularExecution.postFee.ts | 8 +- ...gonPolUsdt0Arb.performExecution.postFee.ts | 37 +- ...sdt0Arb.performModularExecution.postFee.ts | 8 +- .../stargate/polygonUsdcBase.bridge.preFee.ts | 2 +- ...olygonUsdcBase.performExecution.postFee.ts | 135 +- ...sdcBase.performModularExecution.postFee.ts | 8 +- .../swapAndBridge.postFee.balanceOf.ts | 7 +- .../swapAndBridge.postFee.returndata.ts | 7 +- .../swapAndBridge.preFee.balanceOf.ts | 7 +- .../swapAndBridge.preFee.returndata.ts | 7 +- .../e2e/swap/kyberswap.postFee.balanceOf.ts | 10 +- .../e2e/swap/kyberswap.postFee.returndata.ts | 10 +- .../e2e/swap/kyberswap.preFee.balanceOf.ts | 10 +- .../e2e/swap/kyberswap.preFee.returndata.ts | 10 +- scripts/e2e/swap/swap.postFee.balanceOf.ts | 10 +- scripts/e2e/swap/swap.postFee.returndata.ts | 10 +- scripts/e2e/swap/swap.preFee.balanceOf.ts | 10 +- scripts/e2e/swap/swap.preFee.returndata.ts | 10 +- scripts/e2e/swap/zerox.postFee.balanceOf.ts | 10 +- scripts/e2e/swap/zerox.postFee.returndata.ts | 10 +- scripts/e2e/swap/zerox.preFee.balanceOf.ts | 10 +- scripts/e2e/swap/zerox.preFee.returndata.ts | 10 +- scripts/e2e/swapBridgeViaArbitrumNative.ts | 435 ------- scripts/e2e/swapBridgeViaCctp.ts | 556 --------- scripts/e2e/swapBridgeViaCctpSimple.ts | 190 --- scripts/e2e/swapBridgeViaOft.ts | 809 ------------ scripts/e2e/swapBridgeViaStargateNative.ts | 1100 ----------------- scripts/e2e/utils/allowanceHolder.ts | 4 +- scripts/e2e/utils/contractTypes.ts | 100 +- .../e2e/utils/modularActionsBuilder/README.md | 4 +- .../utils/modularActionsBuilder/index.d.ts | 6 +- .../e2e/utils/modularActionsBuilder/index.js | 15 +- scripts/e2e/utils/reproducibility.ts | 60 +- scripts/e2e/utils/routerAbi.ts | 41 +- test/poc/OneInchCctpOpenRouterPoC.t.sol | 16 +- test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 1 + 77 files changed, 959 insertions(+), 4701 deletions(-) create mode 100644 scripts/deploy/deployBungeeOpenRouter.ts delete mode 100644 scripts/deploy/deployBungeeOpenRouterV2.ts delete mode 100644 scripts/e2e/bridgeViaRelay.ts delete mode 100644 scripts/e2e/bridgeViaRelaySimple.ts rename scripts/e2e/{ => misc}/routerUsdc.withdraw.modular.ts (77%) delete mode 100644 scripts/e2e/swapBridgeViaArbitrumNative.ts delete mode 100644 scripts/e2e/swapBridgeViaCctp.ts delete mode 100644 scripts/e2e/swapBridgeViaCctpSimple.ts delete mode 100644 scripts/e2e/swapBridgeViaOft.ts delete mode 100644 scripts/e2e/swapBridgeViaStargateNative.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index 1fbbb90..60d2ffb 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -11,7 +11,7 @@ const accounts = deployerKey ? [deployerKey] : []; const config: HardhatUserConfig = { solidity: { - version: '0.8.25', + version: '0.8.34', settings: { optimizer: { enabled: true, diff --git a/package.json b/package.json index 4756cca..e3000a6 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "compile": "hardhat compile", + "deploy": "hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network", "deploy:v2": "hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network", "typechain": "hardhat typechain", "slither": "bash scripts/docker-slither.sh" diff --git a/scripts/deploy/deployBungeeOpenRouter.ts b/scripts/deploy/deployBungeeOpenRouter.ts new file mode 100644 index 0000000..d5db918 --- /dev/null +++ b/scripts/deploy/deployBungeeOpenRouter.ts @@ -0,0 +1,52 @@ +/** + * Deployment script for BungeeOpenRouter. + * + * Usage: + * npx hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network + * + * Required env vars: + * DEPLOYER_PRIVATE_KEY — deployer wallet private key + + */ + +import hre from 'hardhat'; +import { ethers } from 'hardhat'; + +async function main() { + const [deployer] = await ethers.getSigners(); + const networkName = hre.network.name; + + const owner = deployer.address; + + console.log('Deployer: ', deployer.address); + console.log('Owner: ', owner); + console.log('Network: ', networkName); + console.log(''); + + console.log('Deploying BungeeOpenRouter...'); + const factory = await ethers.getContractFactory('BungeeOpenRouter'); + const router = await factory.deploy(owner); + await router.waitForDeployment(); + const routerAddress = await router.getAddress(); + console.log('BungeeOpenRouter deployed to:', routerAddress); + + console.log('\n=== Deployment Summary ==='); + console.log(`BungeeOpenRouter: ${routerAddress}`); + + const chainId = (await ethers.provider.getNetwork()).chainId; + if (chainId !== 31337n) { + // sleep for 5secs before verification attempt + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // run verification + await hre.run('verify:verify', { + address: routerAddress, + constructorArguments: [owner], + }); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/deploy/deployBungeeOpenRouterV2.ts b/scripts/deploy/deployBungeeOpenRouterV2.ts deleted file mode 100644 index 3d69756..0000000 --- a/scripts/deploy/deployBungeeOpenRouterV2.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Deployment script for BungeeOpenRouterV2 and BungeeOpenRouterV2Unchecked. - * - * Usage: - * npx hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network - * - * Required env vars: - * DEPLOYER_PRIVATE_KEY — deployer wallet private key - * OWNER_ADDRESS — owner of both contracts (defaults to deployer) - * OPEN_ROUTER_SIGNER_ADDRESS — backend signer for BungeeOpenRouterV2 - * - * Optional: set --network to any network configured in hardhat.config.ts. - * Omitting --network runs against the in-process Hardhat network. - */ - -import hre from 'hardhat'; -import { ethers } from 'hardhat'; - -async function main() { - const [deployer] = await ethers.getSigners(); - const networkName = hre.network.name; - - const owner = deployer.address; - const openRouterSigner = deployer.address; - - if (!openRouterSigner) { - throw new Error('OPEN_ROUTER_SIGNER_ADDRESS is not set in environment'); - } - - console.log('Deployer: ', deployer.address); - console.log('Owner: ', owner); - console.log('OpenRouterSigner: ', openRouterSigner); - console.log('Network: ', networkName); - console.log(''); - - // ------------------------------------------------------------------------- - // BungeeOpenRouterV2 (monolithic + modular, signature-verified, AH pull) - // ------------------------------------------------------------------------- - // console.log("Deploying BungeeOpenRouterV2..."); - // const V2Factory = await ethers.getContractFactory("BungeeOpenRouterV2"); - // const v2 = await V2Factory.deploy(owner, openRouterSigner); - // await v2.waitForDeployment(); - // const v2Address = await v2.getAddress(); - // console.log("BungeeOpenRouterV2 deployed to:", v2Address); - - // ------------------------------------------------------------------------- - // BungeeOpenRouterV2Unchecked (same logic, no signature verification) - // ------------------------------------------------------------------------- - console.log('Deploying BungeeOpenRouterV2Unchecked...'); - const V2UFactory = await ethers.getContractFactory( - 'BungeeOpenRouterV2Unchecked', - ); - const v2u = await V2UFactory.deploy(owner); - await v2u.waitForDeployment(); - const v2uAddress = await v2u.getAddress(); - console.log('BungeeOpenRouterV2Unchecked deployed to:', v2uAddress); - - // ------------------------------------------------------------------------- - // Summary - // ------------------------------------------------------------------------- - console.log('\n=== Deployment Summary ==='); - // console.log(`BungeeOpenRouterV2: ${v2Address}`); - console.log(`BungeeOpenRouterV2Unchecked: ${v2uAddress}`); - - // ------------------------------------------------------------------------- - // Verification hint - // ------------------------------------------------------------------------- - const chainId = (await ethers.provider.getNetwork()).chainId; - if (chainId !== 31337n) { - console.log('\nTo verify on a block explorer:'); - // console.log( - // ` npx hardhat verify --network ${networkName} ${v2Address} "${owner}" "${openRouterSigner}"` - // ); - console.log( - ` npx hardhat verify --network ${networkName} ${v2uAddress} "${owner}"`, - ); - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/approveViaModular.ts b/scripts/e2e/approveViaModular.ts index b110e7f..8a62bdb 100644 --- a/scripts/e2e/approveViaModular.ts +++ b/scripts/e2e/approveViaModular.ts @@ -1,10 +1,11 @@ /** * Script — Call ERC-20 approve(spender, amount) through the router using - * `performModularExecution(Action[])`. + * `performActions(Action[])`. * - * This routes a single CALL action targeting the token contract so the router - * itself issues the approval — useful when the router holds tokens and needs - * to authorise a downstream spender (e.g. a bridge contract) before calling it. + * DISABLED by default: `BungeeOpenRouter` now sets max allowance inside + * `swap`, `bridge`, and `swapAndBridge`. Use those entrypoints instead of a + * standalone approval tx. Set `E2E_ENABLE_MODULAR_PRE_APPROVE=1` only if you + * need this legacy helper for manual modular debugging. * * Usage: * TOKEN=0x... SPENDER=0x... AMOUNT=1000000 PRIVATE_KEY=0x... \ @@ -43,6 +44,14 @@ function packActionInfo( // ─── build + send ───────────────────────────────────────────────────────────── async function run(): Promise { + if (process.env.E2E_ENABLE_MODULAR_PRE_APPROVE !== '1') { + console.log( + 'approveViaModular is disabled (router pre-approves in swap/bridge/swapAndBridge).', + ); + console.log('Set E2E_ENABLE_MODULAR_PRE_APPROVE=1 to run this script.'); + return; + } + const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new Error('PRIVATE_KEY env var required'); @@ -96,7 +105,7 @@ async function run(): Promise { }, ]; - const calldata = routerIface.encodeFunctionData('performModularExecution', [ + const calldata = routerIface.encodeFunctionData('performActions', [ ZERO_BYTES32, actions, ]); @@ -111,7 +120,7 @@ async function run(): Promise { amount === ethers.MaxUint256 ? 'MaxUint256' : amount.toString() }`, ); - console.log('Sending performModularExecution → token.approve...'); + console.log('Sending performActions → token.approve...'); const tx = await signer.sendTransaction({ to: routerAddress, diff --git a/scripts/e2e/arbitrum/performExecution.postFee.ts b/scripts/e2e/arbitrum/performExecution.postFee.ts index c05764a..4c009f1 100644 --- a/scripts/e2e/arbitrum/performExecution.postFee.ts +++ b/scripts/e2e/arbitrum/performExecution.postFee.ts @@ -1,10 +1,9 @@ /** * Route: Ethereum AAVE → ETH (OpenOcean) → Arbitrum ETH (inbox depositEth) - * Function: performExecution (monolithic) + * Function: swapAndBridge * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap * - * BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to inbox.depositEth(). - * Input is AAVE (ERC-20) so AllowanceHolder.exec is required. + * BRIDGE_VALUE_FLAG: router forwards swap output as msg.value to inbox.depositEth(). * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performExecution.postFee.ts @@ -30,16 +29,16 @@ import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowance import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { - MonolithicExecutionCall, BRIDGE_VALUE_FLAG, - NO_FEE, + POST_FEE_FLAG, ZERO_ADDRESS, ZERO_BYTES32, - monolithicArgs, + swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +const FLAGS = POST_FEE_FLAG | BRIDGE_VALUE_FLAG; const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); interface OoQuoteResponse { @@ -79,24 +78,6 @@ function buildDepositEthCalldata(): string { ]).encodeFunctionData('depositEth', []); } -async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); - const estimator = new ParentToChildMessageGasEstimator(provider); - const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; - const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); - const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); - const totalFee = BigInt(submissionFee.toString()) + executionCost; - console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); - return totalFee; - } catch (err) { - const fallback = ethers.parseEther('0.001'); - console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); - return fallback; - } -} - async function main() { const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new Error('PRIVATE_KEY env var required'); @@ -125,20 +106,18 @@ async function main() { console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); - const arbFee = await estimateArbitrumBridgeFee(provider); - if (estimatedOut < feeAmount + arbFee) { - console.warn(` Warning: estimated ETH may be insufficient to cover fee + bridge cost`); - } - await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); await ensureRouterNativeBalance(signer, ROUTER_ETH); await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, - preFee: NO_FEE, - swap: { + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, + { receiver: signerAddress, amount: feeAmount }, + { target: ooRouter, approvalSpender: ooRouter, outputToken: NATIVE_TOKEN_ADDRESS, @@ -146,26 +125,21 @@ async function main() { minOutput: minAmountOut, returnDataWordOffset: 0n, }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: 0n }, - flags: BRIDGE_VALUE_FLAG, - }, - swapCallData: swapData, - bridgeCallData: buildDepositEthCalldata(), - }; - - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + swapData, + { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: 0n }, + buildDepositEthCalldata(), + ), + ); await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); const receipt = await execViaAH(signer, ROUTER_ETH, TOKENS.AAVE_ETH, inputAmount, ROUTER_ETH, callData, 0n); - logTxnSummary( - 'Ethereum AAVE → Arbitrum ETH (depositEth) — performExecution postFee', - CHAIN_IDS.ETHEREUM, - receipt, - ); + logTxnSummary('Ethereum AAVE → Arbitrum ETH (depositEth) — swapAndBridge postFee', CHAIN_IDS.ETHEREUM, receipt); console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/arbitrum/performExecution.preFee.ts b/scripts/e2e/arbitrum/performExecution.preFee.ts index 63ff1f4..bbcd01a 100644 --- a/scripts/e2e/arbitrum/performExecution.preFee.ts +++ b/scripts/e2e/arbitrum/performExecution.preFee.ts @@ -1,11 +1,9 @@ /** * Route: Ethereum ETH → Arbitrum ETH (Arbitrum inbox depositEth, no swap) - * Function: performExecution (monolithic) + * Function: bridge * Fee: preFee — FEE_BPS of inputAmount ETH deducted before bridge * - * BRIDGE_VALUE_FLAG set: router forwards the remaining ETH after preFee as - * msg.value to inbox.depositEth(). Input is native ETH so we call execDirect - * (no AllowanceHolder needed — router checks msg.value >= inputAmount directly). + * Input is native ETH — call router.bridge directly (msg.value = inputAmount). * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performExecution.preFee.ts @@ -25,21 +23,12 @@ import { } from '../config'; import { execDirect } from '../utils/allowanceHolder'; import { ROUTER_ABI } from '../utils/routerAbi'; -import { - MonolithicExecutionCall, - BRIDGE_VALUE_FLAG, - NO_FEE, - NO_SWAP, - ZERO_ADDRESS, - ZERO_BYTES32, - monolithicArgs, -} from '../utils/contractTypes'; +import { ZERO_ADDRESS, ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterNativeBalance } from '../utils/reproducibility'; const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); -/** Gas reserve kept in the signer's wallet to cover the transaction itself. */ const GAS_RESERVE = ethers.parseEther('0.005'); function buildDepositEthCalldata(): string { @@ -61,7 +50,9 @@ async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { const arbFee = await estimateArbitrumBridgeFee(provider); if (bridgeValue < arbFee) { - console.warn(` Warning: bridgeValue (${ethers.formatEther(bridgeValue)}) may be below Arbitrum bridge cost (${ethers.formatEther(arbFee)})`); + console.warn( + ` Warning: bridgeValue (${ethers.formatEther(bridgeValue)}) may be below Arbitrum bridge cost (${ethers.formatEther(arbFee)})`, + ); } await ensureRouterNativeBalance(signer, ROUTER_ETH); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, - preFee: { receiver: signerAddress, amount: feeAmount }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: 0n }, - flags: BRIDGE_VALUE_FLAG, - }, - swapCallData: '0x', - bridgeCallData: buildDepositEthCalldata(), - }; + const input: InputData = { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: bridgeValue }; const routerIface = new ethers.Interface(ROUTER_ABI); - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + const callData = routerIface.encodeFunctionData( + 'bridge', + bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, buildDepositEthCalldata()), + ); - // Native ETH input — send directly to the router; no AllowanceHolder needed. - console.log('Sending direct router tx → router.performExecution...'); + console.log('Sending direct router tx → router.bridge...'); const receipt = await execDirect(signer, ROUTER_ETH, callData, inputAmount); - logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — performExecution preFee', CHAIN_IDS.ETHEREUM, receipt); + logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — bridge preFee', CHAIN_IDS.ETHEREUM, receipt); console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); diff --git a/scripts/e2e/arbitrum/performModularExecution.postFee.ts b/scripts/e2e/arbitrum/performModularExecution.postFee.ts index f3c01c6..78a0dcf 100644 --- a/scripts/e2e/arbitrum/performModularExecution.postFee.ts +++ b/scripts/e2e/arbitrum/performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Ethereum AAVE → ETH (OpenOcean) → Arbitrum ETH (inbox depositEth) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap * * Modular action sequence: @@ -13,7 +13,7 @@ * Input is AAVE (ERC-20) so AllowanceHolder.exec is required. * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performActions.postFee.ts */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -145,13 +145,13 @@ async function main() { exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); const receipt = await execViaAH(signer, ROUTER_ETH, TOKENS.AAVE_ETH, inputAmount, ROUTER_ETH, callData, 0n); logTxnSummary( - 'Ethereum AAVE → Arbitrum ETH (depositEth) — performModularExecution postFee', + 'Ethereum AAVE → Arbitrum ETH (depositEth) — performActions postFee', CHAIN_IDS.ETHEREUM, receipt, ); diff --git a/scripts/e2e/arbitrum/performModularExecution.preFee.ts b/scripts/e2e/arbitrum/performModularExecution.preFee.ts index 4048057..ab9c1d9 100644 --- a/scripts/e2e/arbitrum/performModularExecution.preFee.ts +++ b/scripts/e2e/arbitrum/performModularExecution.preFee.ts @@ -1,6 +1,6 @@ /** * Route: Ethereum ETH → Arbitrum ETH (Arbitrum inbox depositEth, no swap) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: preFee — FEE_BPS of inputAmount ETH deducted before bridge * * Modular action sequence: @@ -8,10 +8,10 @@ * [1] nativeCall(inbox, depositEth(), bridgeValue) — bridge remaining ETH * * Input is native ETH so we call execDirect (no AllowanceHolder needed — - * performModularExecution has no _pullFromUser; ETH arrives via msg.value). + * performActions has no _pullFromUser; ETH arrives via msg.value). * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performModularExecution.preFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performActions.preFee.ts */ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; @@ -99,13 +99,13 @@ async function main(): Promise { exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); const routerIface = new ethers.Interface(ROUTER_ABI); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); // Native ETH input — send directly to the router; no AllowanceHolder needed. - console.log('Sending direct router tx → router.performModularExecution...'); + console.log('Sending direct router tx → router.performActions...'); const receipt = await execDirect(signer, ROUTER_ETH, callData, inputAmount); - logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — performModularExecution preFee', CHAIN_IDS.ETHEREUM, receipt); + logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — performActions preFee', CHAIN_IDS.ETHEREUM, receipt); console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); diff --git a/scripts/e2e/bridgeViaRelay.ts b/scripts/e2e/bridgeViaRelay.ts deleted file mode 100644 index 60b7c08..0000000 --- a/scripts/e2e/bridgeViaRelay.ts +++ /dev/null @@ -1,492 +0,0 @@ -/** - * Script 1 — Bridge AAVE (Polygon PoS) → AAVE (Base) via Relay.link - * - * Flow: - * 1. Spend half of the signer’s Polygon AAVE balance twice: first via - * performExecution (monolithic), then via performModularExecution, each leg - * using balance/2 of the initial snapshot. - * 2. For each leg: Relay.link /quote/v2 for AAVE→AAVE cross-chain swap for the - * net relay amount (slice − feeAmount). - * 3. Parse approve + deposit steps → build mono or modular payload. - * 4. AllowanceHolder.exec → router.performExecution / performModularExecution. - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts - * Polygon USDC (Circle) → Base USDC: - * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelay.ts usdc-polygon-base - * - * Router addresses: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain` in config.ts. - * Override Polygon with `ROUTER_CHAIN_137` or legacy `ROUTER_ADDRESS` if needed. - */ -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - FEE_BPS, - bpsOf, - RPC, - ALLOWANCE_HOLDER, -} from './config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; -import { - encodeApprove, - encodeTransfer, - getWalletErc20Balance, -} from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; -import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { MonolithicExecutionCall, NO_FEE, NO_SWAP, ZERO_BYTES32, monolithicArgs } from './utils/contractTypes'; -import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; -import { sleep } from './utils/sleep'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterApproval, -} from './utils/reproducibility'; - -/** Router on Polygon — quotes + modular recipient target must match executing chain deployment. */ -const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); - -function buildMonolithicExecution( - signerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - relaySpender: string, - depositTarget: string, - depositData: string, -): MonolithicExecutionCall { - return { - exec: { - input: { - user: signerAddress, - inputToken: TOKENS.AAVE_POLYGON, - inputAmount, - }, - preFee: { - receiver: signerAddress, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: depositTarget, - approvalSpender: relaySpender, - value: 0n, - }, - flags: 0n, - }, - swapCallData: '0x', - bridgeCallData: depositData, - }; -} - -function buildModularActions( - signerAddress: string, - routerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - bridgeAmount: bigint, - relaySpender: string, - depositTarget: string, - depositData: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.AAVE_POLYGON, - signerAddress, - routerAddress, - inputAmount, - ]); - - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.AAVE_POLYGON, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(relaySpender, bridgeAmount)); - exec.call(depositTarget, depositData); - return exec.toActions(); -} - -async function executeLeg(args: { - label: string; - useModular: boolean; - signer: ethers.Wallet; - signerAddress: string; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { label, useModular, signer, signerAddress, inputAmount, routerIface } = - args; - - const feeAmount = bpsOf(inputAmount, FEE_BPS); - const bridgeAmount = inputAmount - feeAmount; - - console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - console.log(`Input slice: ${ethers.formatUnits(inputAmount, 18)} AAVE`); - console.log( - `Fee (pre-bridge): ${ethers.formatUnits(feeAmount, 18)} (${FEE_BPS} bps)`, - ); - console.log(`Relay amount: ${ethers.formatUnits(bridgeAmount, 18)}`); - - console.log('Fetching Relay.link quote...'); - const quote = await fetchRelayQuoteV2({ - routerAddress: ROUTER_POLYGON, - recipient: signerAddress, - originChainId: CHAIN_IDS.POLYGON, - destinationChainId: CHAIN_IDS.BASE, - originCurrency: TOKENS.AAVE_POLYGON, - destinationCurrency: TOKENS.AAVE_BASE, - amount: bridgeAmount, - }); - const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); - console.log(`Relay spender: ${relaySpender}`); - console.log(`Deposit target: ${depositTarget}`); - - await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, relaySpender); - - let execCalldata: string; - if (useModular) { - const actions = buildModularActions( - signerAddress, - ROUTER_POLYGON, - inputAmount, - feeAmount, - bridgeAmount, - relaySpender, - depositTarget, - depositData, - ); - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - actions, - ]); - } else { - const execPayload = buildMonolithicExecution( - signerAddress, - inputAmount, - feeAmount, - relaySpender, - depositTarget, - depositData, - ); - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(execPayload)); - } - - await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); - - console.log('Sending AllowanceHolder.exec...'); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - TOKENS.AAVE_POLYGON, - inputAmount, - ROUTER_POLYGON, - execCalldata, - ); - - const modeLabel = useModular ? 'Modular' : 'Monolithic'; - logTxnSummary( - `Polygon AAVE → Base AAVE — Relay — ${modeLabel}`, - CHAIN_IDS.POLYGON, - receipt, - ); -} - -function buildMonolithicExecutionUsdcPolygonToBase( - signerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - relaySpender: string, - depositTarget: string, - depositData: string, -): MonolithicExecutionCall { - return { - exec: { - input: { - user: signerAddress, - inputToken: TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - }, - preFee: { - receiver: signerAddress, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: depositTarget, - approvalSpender: relaySpender, - value: 0n, - }, - flags: 0n, - }, - swapCallData: '0x', - bridgeCallData: depositData, - }; -} - -function buildModularActionsUsdcPolygonToBase( - signerAddress: string, - routerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - bridgeAmount: bigint, - relaySpender: string, - depositTarget: string, - depositData: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.USDC_POLYGON_CIRCLE, - signerAddress, - routerAddress, - inputAmount, - ]); - - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(relaySpender, bridgeAmount)); - exec.call(depositTarget, depositData); - return exec.toActions(); -} - -async function executeLegUsdcPolygonToBase(args: { - label: string; - useModular: boolean; - signer: ethers.Wallet; - signerAddress: string; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { label, useModular, signer, signerAddress, inputAmount, routerIface } = - args; - - const feeAmount = bpsOf(inputAmount, FEE_BPS); - const bridgeAmount = inputAmount - feeAmount; - - console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - console.log(`Input slice: ${ethers.formatUnits(inputAmount, 6)} USDC`); - console.log( - `Fee (pre-bridge): ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`, - ); - console.log(`Relay amount: ${ethers.formatUnits(bridgeAmount, 6)}`); - - console.log('Fetching Relay.link quote...'); - const quote = await fetchRelayQuoteV2({ - routerAddress: ROUTER_POLYGON, - recipient: signerAddress, - originChainId: CHAIN_IDS.POLYGON, - destinationChainId: CHAIN_IDS.BASE, - originCurrency: TOKENS.USDC_POLYGON_CIRCLE, - destinationCurrency: TOKENS.USDC_BASE, - amount: bridgeAmount, - }); - const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); - console.log(`Relay spender: ${relaySpender}`); - console.log(`Deposit target: ${depositTarget}`); - - await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, relaySpender); - - let execCalldata: string; - if (useModular) { - const actions = buildModularActionsUsdcPolygonToBase( - signerAddress, - ROUTER_POLYGON, - inputAmount, - feeAmount, - bridgeAmount, - relaySpender, - depositTarget, - depositData, - ); - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - actions, - ]); - } else { - const execPayload = buildMonolithicExecutionUsdcPolygonToBase( - signerAddress, - inputAmount, - feeAmount, - relaySpender, - depositTarget, - depositData, - ); - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(execPayload)); - } - - await ensureAllowanceForAllowanceHolder( - signer, - TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - ); - - console.log('Sending AllowanceHolder.exec...'); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - ROUTER_POLYGON, - execCalldata, - ); - - const modeLabel = useModular ? 'Modular' : 'Monolithic'; - logTxnSummary( - `Polygon USDC → Base USDC — Relay — ${modeLabel}`, - CHAIN_IDS.POLYGON, - receipt, - ); -} - -async function mainUsdcPolygonToBaseRelay() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.USDC_POLYGON_CIRCLE; - const { balance: walletBalance } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with Polygon native Circle USDC first.`, - ); - } - - const legAmount = (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error( - `Wallet balance (${walletBalance}) is too small to split into two nonzero halves.`, - ); - } - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`Input token: ${inputToken}`); - console.log(`Full balance: ${ethers.formatUnits(walletBalance, 6)} USDC`); - console.log( - `Per execution: ${ethers.formatUnits(legAmount, 6)} USDC (50% snapshots)`, - ); - - const routerIface = new ethers.Interface(ROUTER_ABI); - - await executeLegUsdcPolygonToBase({ - label: '1/2 Monolithic', - useModular: false, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log('Sleeping ~3s before modular execution...'); - await sleep(3000); - - await executeLegUsdcPolygonToBase({ - label: '2/2 Modular', - useModular: true, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log( - '\nCompleted monolithic then modular executions (Relay Polygon USDC → Base USDC).', - ); -} - -async function main() { - const relayE2eCase = process.argv[2]?.toLowerCase(); - if (relayE2eCase === 'usdc-polygon-base' || relayE2eCase === 'usdc') { - await mainUsdcPolygonToBaseRelay(); - return; - } - - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.AAVE_POLYGON; - const { balance: walletBalance } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero balance of ${inputToken}. Fund the wallet with Polygon AAVE first.`, - ); - } - - const legAmount = (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error( - `Wallet balance (${walletBalance}) is too small to split into two nonzero halves.`, - ); - } - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`Input token: ${inputToken}`); - console.log(`Full balance: ${ethers.formatUnits(walletBalance, 18)} AAVE`); - console.log( - `Per execution: ${ethers.formatUnits(legAmount, 18)} AAVE (50% snapshots)`, - ); - - const routerIface = new ethers.Interface(ROUTER_ABI); - - await executeLeg({ - label: '1/2 Monolithic', - useModular: false, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log('Sleeping ~3s before modular execution...'); - await sleep(3000); - - await executeLeg({ - label: '2/2 Modular', - useModular: true, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log( - '\nCompleted monolithic then modular executions (Relay Polygon → Base AAVE).', - ); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/bridgeViaRelaySimple.ts b/scripts/e2e/bridgeViaRelaySimple.ts deleted file mode 100644 index 4a7a78f..0000000 --- a/scripts/e2e/bridgeViaRelaySimple.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Script — Bridge AAVE (Polygon PoS) → AAVE (Base) via Relay.link - * using the `bridge(InputData, FeeData, BridgeData, bytes)` entrypoint. - * - * Flow: - * 1. Read signer's Polygon AAVE (or USDC) balance. - * 2. Compute fee via FEE_BPS; set fee.amount=0 to skip the fee entirely. - * 3. Fetch Relay.link /quote/v2 for the net bridge amount (inputAmount − fee). - * 4. AllowanceHolder.exec → router.bridge(input, fee, bridgeData, bridgeCallData). - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts - * - * USDC path (Polygon Circle USDC → Base USDC): - * PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts usdc - * - * No fee: - * FEE_AMOUNT_BPS=0 PRIVATE_KEY=0x... ts-node scripts/e2e/bridgeViaRelaySimple.ts - * - * Router addresses: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain` in config.ts. - * Override with `ROUTER_CHAIN_137` or legacy `ROUTER_ADDRESS` if needed. - */ -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - FEE_BPS, - bpsOf, - RPC, -} from './config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; -import { getWalletErc20Balance } from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ZERO_BYTES32 } from './utils/contractTypes'; -import type { BridgeData, FeeData, InputData } from './utils/contractTypes'; -import { fetchRelayQuoteV2, parseRelayQuote } from './utils/relayLinkQuote'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterApproval, -} from './utils/reproducibility'; - -const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); - -// ─── Execution builder ──────────────────────────────────────────────────────── - -interface BridgeParams { - signerAddress: string; - inputToken: string; - inputAmount: bigint; - fee: FeeData; - relaySpender: string; - depositTarget: string; - /** Bridge calldata with finalAmount (= inputAmount - feeAmount) already encoded inside. */ - depositData: string; -} - -function buildBridgeCalldata(routerIface: ethers.Interface, p: BridgeParams): string { - const input: InputData = { - user: p.signerAddress, - inputToken: p.inputToken, - inputAmount: p.inputAmount, - }; - - // No bridge amount-position flag or bridge-value flag — the caller bakes the net amount - // directly into depositData before calling. - const bridgeData: BridgeData = { - target: p.depositTarget, - approvalSpender: p.relaySpender, - value: 0n, - }; - - return routerIface.encodeFunctionData('bridge', [ - ZERO_BYTES32, - input, - p.fee, - bridgeData, - p.depositData, - ]); -} - -// ─── Execution leg ──────────────────────────────────────────────────────────── - -interface LegConfig { - label: string; - inputToken: string; - decimals: number; - symbol: string; - originChainId: number; - destinationChainId: number; - destinationCurrency: string; -} - -async function executeLeg(args: { - config: LegConfig; - signer: ethers.Wallet; - signerAddress: string; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { config, signer, signerAddress, inputAmount, routerIface } = args; - - const feeAmount = bpsOf(inputAmount, FEE_BPS); - const bridgeAmount = inputAmount - feeAmount; - - const fmt = (n: bigint) => ethers.formatUnits(n, config.decimals); - - console.log(`\n── ${config.label} ──`); - console.log(`Input: ${fmt(inputAmount)} ${config.symbol}`); - console.log(`Fee (${FEE_BPS} bps): ${fmt(feeAmount)} ${config.symbol}`); - console.log(`Bridge amount: ${fmt(bridgeAmount)} ${config.symbol}`); - - const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - console.log(`Fee tuple: ${fee.amount === 0n ? 'amount=0 (no fee)' : `receiver=${fee.receiver}, amount=${fee.amount}`}`); - - console.log('Fetching Relay.link quote...'); - const quote = await fetchRelayQuoteV2({ - routerAddress: ROUTER_POLYGON, - recipient: signerAddress, - originChainId: config.originChainId, - destinationChainId: config.destinationChainId, - originCurrency: config.inputToken, - destinationCurrency: config.destinationCurrency, - amount: bridgeAmount, - }); - const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); - console.log(`Relay spender: ${relaySpender}`); - console.log(`Deposit target: ${depositTarget}`); - - await ensureRouterErc20Balance(signer, config.inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, config.inputToken, relaySpender); - - const execCalldata = buildBridgeCalldata(routerIface, { - signerAddress, - inputToken: config.inputToken, - inputAmount, - fee, - relaySpender, - depositTarget, - depositData, - }); - - await ensureAllowanceForAllowanceHolder(signer, config.inputToken, inputAmount); - - console.log('Sending AllowanceHolder.exec → router.bridge...'); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - config.inputToken, - inputAmount, - ROUTER_POLYGON, - execCalldata, - ); - - logTxnSummary(config.label, config.originChainId, receipt); -} - -// ─── Entry points ───────────────────────────────────────────────────────────── - -async function run(useUsdc: boolean): Promise { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const legConfig: LegConfig = useUsdc - ? { - label: 'Polygon USDC → Base USDC — Relay — Simple Bridge', - inputToken: TOKENS.USDC_POLYGON_CIRCLE, - decimals: 6, - symbol: 'USDC', - originChainId: CHAIN_IDS.POLYGON, - destinationChainId: CHAIN_IDS.BASE, - destinationCurrency: TOKENS.USDC_BASE, - } - : { - label: 'Polygon AAVE → Base AAVE — Relay — Simple Bridge', - inputToken: TOKENS.AAVE_POLYGON, - decimals: 18, - symbol: 'AAVE', - originChainId: CHAIN_IDS.POLYGON, - destinationChainId: CHAIN_IDS.BASE, - destinationCurrency: TOKENS.AAVE_BASE, - }; - - const { balance: walletBalance } = await getWalletErc20Balance( - legConfig.inputToken, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero balance of ${legConfig.inputToken}. Fund the wallet first.`, - ); - } - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`Input token: ${legConfig.inputToken}`); - console.log( - `Balance: ${ethers.formatUnits(walletBalance, legConfig.decimals)} ${legConfig.symbol}`, - ); - - const routerIface = new ethers.Interface(ROUTER_ABI); - - await executeLeg({ - config: legConfig, - signer, - signerAddress, - inputAmount: (walletBalance - 20n) / 2n, - routerIface, - }); - - console.log('\nDone.'); -} - -async function main(): Promise { - const arg = process.argv[2]?.toLowerCase(); - const useUsdc = arg === 'usdc' || arg === 'usdc-polygon-base'; - await run(useUsdc); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/cctp/bridge.preFee.ts b/scripts/e2e/cctp/bridge.preFee.ts index 196daf6..ce85682 100644 --- a/scripts/e2e/cctp/bridge.preFee.ts +++ b/scripts/e2e/cctp/bridge.preFee.ts @@ -4,7 +4,7 @@ * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge * * Bridge amount is pre-encoded in depositForBurn calldata (no splice needed). - * Uses router.bridge() rather than performExecution / performModularExecution. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/bridge.preFee.ts diff --git a/scripts/e2e/cctp/performExecution.postFee.ts b/scripts/e2e/cctp/performExecution.postFee.ts index 45d8544..645346a 100644 --- a/scripts/e2e/cctp/performExecution.postFee.ts +++ b/scripts/e2e/cctp/performExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) - * Function: performExecution (monolithic) + * Function: swapAndBridge * Fee: postFee — FEE_BPS of estimatedOut USDC deducted after swap * * Usage: @@ -26,15 +26,15 @@ import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowance import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { - MonolithicExecutionCall, - NO_FEE, + POST_FEE_FLAG, ZERO_BYTES32, bridgeAmountPositionFlag, - monolithicArgs, + swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(4); const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); interface OoQuoteResponse { @@ -124,11 +124,14 @@ async function main() { await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount }, - preFee: NO_FEE, - swap: { + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount }, + { receiver: signerAddress, amount: feeAmount }, + { target: ooRouter, approvalSpender: ooRouter, outputToken: TOKENS.USDC_POLYGON_CIRCLE, @@ -136,26 +139,21 @@ async function main() { minOutput: minAmountOut, returnDataWordOffset: 0n, }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, - flags: bridgeAmountPositionFlag(4), - }, - swapCallData: swapData, - bridgeCallData: depositForBurnData, - }; - - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + swapData, + { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, + depositForBurnData, + ), + ); await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData); - logTxnSummary( - 'Polygon AAVE → Base USDC (CCTP) — performExecution postFee', - CHAIN_IDS.POLYGON, - receipt, - ); + logTxnSummary('Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee', CHAIN_IDS.POLYGON, receipt); console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/performExecution.preFee.ts b/scripts/e2e/cctp/performExecution.preFee.ts index 07808f6..5065564 100644 --- a/scripts/e2e/cctp/performExecution.preFee.ts +++ b/scripts/e2e/cctp/performExecution.preFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon USDC → Base USDC (CCTP depositForBurn, no swap) - * Function: performExecution (monolithic) + * Function: bridge * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge * * Usage: @@ -23,12 +23,11 @@ import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowance import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, + bridgeArgs, + type BridgeData, + type FeeData, + type InputData, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; @@ -39,12 +38,13 @@ function buildDepositForBurnCalldata( recipientAddress: string, burnToken: string, destinationCctpDomain: number, + amount: bigint, ): string { const iface = new ethers.Interface([ 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', ]); return iface.encodeFunctionData('depositForBurn', [ - 0n, + amount, destinationCctpDomain, ethers.zeroPadValue(recipientAddress, 32), burnToken, @@ -69,47 +69,46 @@ async function main() { if (inputAmount === 0n) throw new Error('Balance too small'); const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; console.log(`Signer: ${signerAddress}`); console.log(`Router: ${ROUTER_POLYGON}`); console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); - console.log(`Net to bridge: ${ethers.formatUnits(inputAmount - feeAmount, 6)}`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); - const routerIface = new ethers.Interface(ROUTER_ABI); const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + bridgeAmount, + ); - const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + const input: InputData = { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { + target: polyCctp.tokenMessenger, + approvalSpender: polyCctp.tokenMessenger, + value: 0n, + }; await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }, - preFee: { receiver: signerAddress, amount: feeAmount }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, - flags: bridgeAmountPositionFlag(4), - }, - swapCallData: '0x', - bridgeCallData: depositForBurnData, - }; - - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + const routerIface = new ethers.Interface(ROUTER_ABI); + const callData = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositForBurnData)); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); - logTxnSummary( - 'Polygon USDC → Base USDC (CCTP) — performExecution preFee', - CHAIN_IDS.POLYGON, - receipt, - ); + logTxnSummary('Polygon USDC → Base USDC (CCTP) — bridge preFee', CHAIN_IDS.POLYGON, receipt); console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/performModularExecution.postFee.ts b/scripts/e2e/cctp/performModularExecution.postFee.ts index 719e0b9..7d37635 100644 --- a/scripts/e2e/cctp/performModularExecution.postFee.ts +++ b/scripts/e2e/cctp/performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of estimatedOut USDC transferred to signer after swap * * Modular action sequence: @@ -13,7 +13,7 @@ * [6] tokenMessenger.depositForBurn(...) — spliceArg(0) patches amount from [5] * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performActions.postFee.ts */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -141,13 +141,13 @@ async function main() { const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData); logTxnSummary( - 'Polygon AAVE → Base USDC (CCTP) — performModularExecution postFee', + 'Polygon AAVE → Base USDC (CCTP) — performActions postFee', CHAIN_IDS.POLYGON, receipt, ); diff --git a/scripts/e2e/cctp/performModularExecution.preFee.ts b/scripts/e2e/cctp/performModularExecution.preFee.ts index b432ff9..89582a2 100644 --- a/scripts/e2e/cctp/performModularExecution.preFee.ts +++ b/scripts/e2e/cctp/performModularExecution.preFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon USDC → Base USDC (CCTP depositForBurn, no swap) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: preFee — FEE_BPS of inputAmount USDC transferred to signer before bridge * * Modular action sequence: @@ -11,7 +11,7 @@ * [4] tokenMessenger.depositForBurn(...) — spliceArg(0) patches amount from [3] * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performModularExecution.preFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performActions.preFee.ts */ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; @@ -97,13 +97,13 @@ async function main() { const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); logTxnSummary( - 'Polygon USDC → Base USDC (CCTP) — performModularExecution preFee', + 'Polygon USDC → Base USDC (CCTP) — performActions preFee', CHAIN_IDS.POLYGON, receipt, ); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts index f622b53..1a7fe75 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -162,14 +162,14 @@ async function main() { polyCctp.tokenMessenger ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -186,7 +186,7 @@ async function main() { value: 0n, }, depositForBurnData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts index b09b480..3a8abf6 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -216,14 +216,14 @@ async function main() { polyCctp.tokenMessenger ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, @@ -240,7 +240,7 @@ async function main() { value: 0n, }, depositForBurnData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts index c12c1ba..dffa1df 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -162,14 +162,14 @@ async function main() { polyCctp.tokenMessenger ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -186,7 +186,7 @@ async function main() { value: 0n, }, depositForBurnData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts index 78ba078..7d0b75c 100644 --- a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -164,14 +164,14 @@ async function main() { polyCctp.tokenMessenger ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -188,7 +188,7 @@ async function main() { value: 0n, }, depositForBurnData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts index c72b884..f9729ac 100644 --- a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -164,14 +164,14 @@ async function main() { polyCctp.tokenMessenger ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -188,7 +188,7 @@ async function main() { value: 0n, }, depositForBurnData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index a285251..24f4dc6 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -34,7 +34,7 @@ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. */ export const ROUTER_BY_CHAIN_ID: Record = { - [CHAIN_IDS.POLYGON]: '0x5abf9dccabc44ea9421f1e1Fbd6BA6A4f2387342', + [CHAIN_IDS.POLYGON]: '0x7894c2c93e8952867e2fA4C0778296fEE77074Ea', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', [CHAIN_IDS.BASE]: '0x91b536E79cd3607b593f3011937862609D608253', [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', diff --git a/scripts/e2e/routerUsdc.withdraw.modular.ts b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts similarity index 77% rename from scripts/e2e/routerUsdc.withdraw.modular.ts rename to scripts/e2e/misc/routerUsdc.withdraw.modular.ts index 210307d..4df585d 100644 --- a/scripts/e2e/routerUsdc.withdraw.modular.ts +++ b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts @@ -1,6 +1,6 @@ /** - * Polygon: sweep USDC from `BungeeOpenRouterV2Unchecked` to the tx sender using - * `performModularExecution` only — no AllowanceHolder, no pull step. + * Polygon: sweep USDC from `BungeeOpenRouter` to the tx sender using + * `performActions` only — no AllowanceHolder, no pull step. * * Actions: * [0] STATICCALL USDC.balanceOf(router) — stored returndata (32-byte uint256) @@ -8,7 +8,7 @@ * is transferring the router's entire USDC balance to `msg.sender` of this tx. * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/polygon/routerUsdc.withdraw.modular.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/misc/routerUsdc.withdraw.modular.ts * * Requires the router contract to actually hold Polygon USDC * ({@link TOKENS.USDC_POLYGON_CIRCLE}). @@ -17,16 +17,16 @@ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; dotenv.config(); -import { CHAIN_IDS, routerAddressForChain, TOKENS, RPC } from './config'; +import { CHAIN_IDS, routerAddressForChain, TOKENS, RPC } from '../config'; import { encodeBalanceOf, encodeTransfer, getWalletErc20Balance, -} from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; -import { ZERO_BYTES32 } from './utils/contractTypes'; -import { logTxnSummary } from './utils/txnLogSummary'; +} from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; async function main(): Promise { const privateKey = process.env.PRIVATE_KEY; @@ -64,13 +64,13 @@ async function main(): Promise { .spliceArg(1, routerBal.ref().returnWord(0)); const routerIface = new ethers.Interface(ROUTER_ABI); - const calldata = routerIface.encodeFunctionData('performModularExecution', [ + const calldata = routerIface.encodeFunctionData('performActions', [ ZERO_BYTES32, exec.toActions(), ]); console.log( - 'Sending performModularExecution (balanceOf → transfer with spliced amount)...', + 'Sending performActions (balanceOf → transfer with spliced amount)...', ); const tx = await signer.sendTransaction({ to: routerAddress, @@ -84,7 +84,7 @@ async function main(): Promise { } logTxnSummary( - 'Polygon — withdraw router USDC to caller via performModularExecution', + 'Polygon — withdraw router USDC to caller via performActions', chainId, receipt, ); diff --git a/scripts/e2e/oft/bridge.preFee.ts b/scripts/e2e/oft/bridge.preFee.ts index e1f1033..49a909f 100644 --- a/scripts/e2e/oft/bridge.preFee.ts +++ b/scripts/e2e/oft/bridge.preFee.ts @@ -5,7 +5,7 @@ * * Bridge amount is pre-encoded in OFT send() calldata (no splice needed). * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. - * Uses router.bridge() rather than performExecution / performModularExecution. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/bridge.preFee.ts diff --git a/scripts/e2e/oft/performExecution.postFee.ts b/scripts/e2e/oft/performExecution.postFee.ts index eb4a2d1..67c0bbb 100644 --- a/scripts/e2e/oft/performExecution.postFee.ts +++ b/scripts/e2e/oft/performExecution.postFee.ts @@ -1,18 +1,18 @@ /** * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) - * Function: performExecution (monolithic) - * Fee: postFee — FEE_BPS of estimatedOut USDT0 deducted after swap + * Flags: post-fee (fee taken from USDT0 output after swap), output read from swap returndata word 0 + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) * - * Bridge amount position flag splices actual post-fee balance into send() amountLD at byte 196. - * bridge.value = nativeFeeWithBuffer (5% buffer on LZ fee) forwarded as LZ msg.value. + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDT0, deducted from swap output. + * Returndata (bit1=0): final USDT0 amount is read from word 0 of the swap call returndata. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performExecution.postFee.ts */ -import axios from 'axios'; -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -import { Options } from '@layerzerolabs/lz-v2-utilities'; +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; dotenv.config(); import { @@ -26,29 +26,33 @@ import { OO_SLIPPAGE_PERCENT, ARBITRUM_LZ_EID, USDT0_OFT_ADAPTER_POLYGON, -} from '../config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { getWalletErc20Balance } from '../utils/erc20'; -import { ROUTER_ABI } from '../utils/routerAbi'; +} from "../config"; import { - MonolithicExecutionCall, - NO_FEE, - ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, -} from '../utils/contractTypes'; -import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterApproval, +} from "../utils/reproducibility"; +// post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x01n | bridgeAmountPositionFlag(196); const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); -const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); -const OFT_AMOUNT_LD_OFFSET = 196; +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); const OFT_ABI = [ - 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', - 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', - 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", ]; + const OFT_IFACE = new ethers.Interface(OFT_ABI); interface OoQuoteResponse { @@ -68,7 +72,7 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ slippage: OO_SLIPPAGE_PERCENT, sender: ROUTER_POLYGON, account: ROUTER_POLYGON, - gasPrice: '1', + gasPrice: "1", }; if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; @@ -85,21 +89,45 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ async function fetchOftQuote( provider: ethers.JsonRpcProvider, bridgeAmountLD: bigint, - recipient: string, + recipient: string ): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { - const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); const to32 = ethers.zeroPadValue(recipient, 32); - const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; - const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; return { - nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, }; } function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { - return OFT_IFACE.encodeFunctionData('send', [ - { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, { nativeFee, lzTokenFee: 0n }, recipient, ]); @@ -107,77 +135,128 @@ function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { async function main() { const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); const provider = new ethers.JsonRpcProvider(RPC.POLYGON); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); - const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); - if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); const inputAmount = walletBalance - 20n; - if (inputAmount === 0n) throw new Error('Balance too small'); + if (inputAmount === 0n) throw new Error("Balance too small"); console.log(`Signer: ${signerAddress}`); console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=196)` + ); console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); const routerIface = new ethers.Interface(ROUTER_ABI); - console.log('Fetching OpenOcean quote (AAVE → USDT0)...'); - const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDT0 output after swap const feeAmount = bpsOf(estimatedOut, FEE_BPS); const bridgeAmount = estimatedOut - feeAmount; console.log(` OO router: ${ooRouter}`); console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); - console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatUnits(bridgeAmount, 6)}`); console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); - console.log('Fetching OFT quote (Polygon → Arbitrum)...'); - const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); - console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter + ); + await ensureRouterApproval( + signer, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON + ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: TOKENS.USDT0_POLYGON, - value: 0n, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, - flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, }, - swapCallData: swapData, - bridgeCallData: oftSendData, - }; - - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, + }, + oftSendData, + )); - await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); - const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); logTxnSummary( - 'Polygon AAVE → Arbitrum USDT0 (OFT) — performExecution postFee', + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge postFee/returndata`, CHAIN_IDS.POLYGON, - receipt, + receipt ); - console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/performExecution.preFee.ts b/scripts/e2e/oft/performExecution.preFee.ts index a26827f..ebb6ab8 100644 --- a/scripts/e2e/oft/performExecution.preFee.ts +++ b/scripts/e2e/oft/performExecution.preFee.ts @@ -1,10 +1,11 @@ /** * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) - * Function: performExecution (monolithic) + * Function: bridge (simple bridge entrypoint) * Fee: preFee — FEE_BPS of inputAmount USDT0 deducted before bridge * - * Bridge amount position flag splices actual post-fee balance into send() amountLD at byte 196. + * Bridge amount is pre-encoded in OFT send() calldata (no splice needed). * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performExecution.preFee.ts @@ -27,20 +28,12 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; -import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, -} from '../utils/contractTypes'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); -const OFT_AMOUNT_LD_OFFSET = 196; const OFT_ABI = [ 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', @@ -56,32 +49,60 @@ async function fetchOftQuote( ): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); const to32 = ethers.zeroPadValue(recipient, 32); - const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; - const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); return { nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, }; } -function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { +/** + * Builds OFT send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildOftSendCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { return OFT_IFACE.encodeFunctionData('send', [ - { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }, { nativeFee, lzTokenFee: 0n }, recipient, ]); } -async function main() { +async function main(): Promise { const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } const provider = new ethers.JsonRpcProvider(RPC.POLYGON); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); - const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDT0_POLYGON, signerAddress, provider); - if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + const inputToken = TOKENS.USDT0_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + } const inputAmount = walletBalance - 20n; if (inputAmount === 0n) throw new Error('Balance too small'); @@ -89,49 +110,51 @@ async function main() { const feeAmount = bpsOf(inputAmount, FEE_BPS); const bridgeAmount = inputAmount - feeAmount; - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); - console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); - console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); - - const routerIface = new ethers.Interface(ROUTER_ABI); + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); console.log('Fetching OFT quote (Polygon → Arbitrum)...'); const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); - console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); - await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); - - const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + const sendData = buildOftSendCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.USDT0_POLYGON, inputAmount }, - preFee: { receiver: signerAddress, amount: feeAmount }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, - flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), - }, - swapCallData: '0x', - bridgeCallData: oftSendData, + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + value: nativeFeeWithBuffer, }; - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); - - await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDT0_POLYGON, inputAmount); - const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); - - logTxnSummary( - 'Polygon USDT0 → Arbitrum USDT0 (OFT direct) — performExecution preFee', - CHAIN_IDS.POLYGON, - receipt, + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, USDT0_OFT_ADAPTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, ); + logTxnSummary('Polygon USDT0 → Arbitrum USDT0 (OFT) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/performModularExecution.postFee.ts b/scripts/e2e/oft/performModularExecution.postFee.ts index e59d095..74e7e59 100644 --- a/scripts/e2e/oft/performModularExecution.postFee.ts +++ b/scripts/e2e/oft/performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of estimatedOut USDT0 transferred to signer after swap * * Modular action sequence: @@ -13,7 +13,7 @@ * [6] nativeCall adapter.send(...) — spliceWord patches amountLD at offset 196 from [5] * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performActions.postFee.ts */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -161,13 +161,13 @@ async function main() { exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); logTxnSummary( - 'Polygon AAVE → Arbitrum USDT0 (OFT) — performModularExecution postFee', + 'Polygon AAVE → Arbitrum USDT0 (OFT) — performActions postFee', CHAIN_IDS.POLYGON, receipt, ); diff --git a/scripts/e2e/oft/performModularExecution.preFee.ts b/scripts/e2e/oft/performModularExecution.preFee.ts index 097ef85..4f02cdc 100644 --- a/scripts/e2e/oft/performModularExecution.preFee.ts +++ b/scripts/e2e/oft/performModularExecution.preFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: preFee — FEE_BPS of inputAmount USDT0 transferred to signer before bridge * * Modular action sequence: @@ -11,7 +11,7 @@ * [4] nativeCall adapter.send(...) — spliceWord patches amountLD at offset 196 from [3] * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performModularExecution.preFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performActions.preFee.ts */ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; @@ -117,13 +117,13 @@ async function main() { exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDT0_POLYGON, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); logTxnSummary( - 'Polygon USDT0 → Arbitrum USDT0 (OFT direct) — performModularExecution preFee', + 'Polygon USDT0 → Arbitrum USDT0 (OFT direct) — performActions preFee', CHAIN_IDS.POLYGON, receipt, ); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts index b291e9b..d865f0e 100644 --- a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts @@ -33,7 +33,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -206,14 +206,14 @@ async function main() { const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -230,7 +230,7 @@ async function main() { value: nativeFeeWithBuffer, }, oftSendData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts index 7d76600..fad5ebb 100644 --- a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts @@ -33,7 +33,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -206,14 +206,14 @@ async function main() { const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -230,7 +230,7 @@ async function main() { value: nativeFeeWithBuffer, }, oftSendData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts index f1e91ab..789858f 100644 --- a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts @@ -33,7 +33,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -205,14 +205,14 @@ async function main() { const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -229,7 +229,7 @@ async function main() { value: nativeFeeWithBuffer, }, oftSendData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts index 5e6353b..e8a60ae 100644 --- a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts @@ -33,7 +33,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32, bridgeAmountPositionFlag } from "../utils/contractTypes"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -205,14 +205,14 @@ async function main() { const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -229,7 +229,7 @@ async function main() { value: nativeFeeWithBuffer, }, oftSendData, - ]); + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/relay/aave.bridge.preFee.ts b/scripts/e2e/relay/aave.bridge.preFee.ts index 15efa33..2d90da3 100644 --- a/scripts/e2e/relay/aave.bridge.preFee.ts +++ b/scripts/e2e/relay/aave.bridge.preFee.ts @@ -4,7 +4,7 @@ * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge * * Bridge amount is pre-encoded in Relay deposit calldata. - * Uses router.bridge() rather than performExecution / performModularExecution. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.bridge.preFee.ts diff --git a/scripts/e2e/relay/aave.performExecution.preFee.ts b/scripts/e2e/relay/aave.performExecution.preFee.ts index ea6ea8c..188ba82 100644 --- a/scripts/e2e/relay/aave.performExecution.preFee.ts +++ b/scripts/e2e/relay/aave.performExecution.preFee.ts @@ -1,11 +1,8 @@ /** * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) - * Function: performExecution (monolithic) + * Function: bridge * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge * - * Fetches a Relay.link /quote/v2 for the net bridge amount, then encodes a - * MonolithicExecutionCall with preFee and the deposit calldata. - * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performExecution.preFee.ts */ @@ -24,13 +21,7 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; -import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_BYTES32, - monolithicArgs, -} from '../utils/contractTypes'; +import { ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; @@ -82,28 +73,19 @@ async function main(): Promise { await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken, inputAmount }, - preFee: { receiver: signerAddress, amount: feeAmount }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { target: depositTarget, approvalSpender: relaySpender, value: 0n }, - flags: 0n, - }, - swapCallData: '0x', - bridgeCallData: depositData, - }; + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); - const execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + const execCalldata = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositData)); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - console.log('Sending AllowanceHolder.exec → router.performExecution...'); + console.log('Sending AllowanceHolder.exec → router.bridge...'); const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); - logTxnSummary('Polygon AAVE → Base AAVE — Relay — performExecution preFee', CHAIN_IDS.POLYGON, receipt); + logTxnSummary('Polygon AAVE → Base AAVE — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); } main().catch((err) => { diff --git a/scripts/e2e/relay/aave.performModularExecution.preFee.ts b/scripts/e2e/relay/aave.performModularExecution.preFee.ts index eaa5dd0..42076a0 100644 --- a/scripts/e2e/relay/aave.performModularExecution.preFee.ts +++ b/scripts/e2e/relay/aave.performModularExecution.preFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge * * Modular action sequence: @@ -10,7 +10,7 @@ * [3] call(depositTarget, depositData) — Relay bridge * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performModularExecution.preFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performActions.preFee.ts */ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; @@ -91,14 +91,14 @@ async function main(): Promise { exec.call(depositTarget, depositData); const routerIface = new ethers.Interface(ROUTER_ABI); - const execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const execCalldata = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - console.log('Sending AllowanceHolder.exec → router.performModularExecution...'); + console.log('Sending AllowanceHolder.exec → router.performActions...'); const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); - logTxnSummary('Polygon AAVE → Base AAVE — Relay — performModularExecution preFee', CHAIN_IDS.POLYGON, receipt); + logTxnSummary('Polygon AAVE → Base AAVE — Relay — performActions preFee', CHAIN_IDS.POLYGON, receipt); } main().catch((err) => { diff --git a/scripts/e2e/relay/usdc.bridge.preFee.ts b/scripts/e2e/relay/usdc.bridge.preFee.ts index 3b374b6..438d915 100644 --- a/scripts/e2e/relay/usdc.bridge.preFee.ts +++ b/scripts/e2e/relay/usdc.bridge.preFee.ts @@ -4,7 +4,7 @@ * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge * * Bridge amount is pre-encoded in Relay deposit calldata. - * Uses router.bridge() rather than performExecution / performModularExecution. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.bridge.preFee.ts diff --git a/scripts/e2e/relay/usdc.performExecution.preFee.ts b/scripts/e2e/relay/usdc.performExecution.preFee.ts index 9be5f2b..baf4aff 100644 --- a/scripts/e2e/relay/usdc.performExecution.preFee.ts +++ b/scripts/e2e/relay/usdc.performExecution.preFee.ts @@ -1,11 +1,8 @@ /** * Route: Polygon USDC → Base USDC via Relay.link (no swap) - * Function: performExecution (monolithic) + * Function: bridge * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge * - * Fetches a Relay.link /quote/v2 for the net bridge amount, then encodes a - * MonolithicExecutionCall with preFee and the deposit calldata. - * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performExecution.preFee.ts */ @@ -24,13 +21,7 @@ import { import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; -import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_BYTES32, - monolithicArgs, -} from '../utils/contractTypes'; +import { ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; @@ -82,28 +73,19 @@ async function main(): Promise { await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken, inputAmount }, - preFee: { receiver: signerAddress, amount: feeAmount }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { target: depositTarget, approvalSpender: relaySpender, value: 0n }, - flags: 0n, - }, - swapCallData: '0x', - bridgeCallData: depositData, - }; + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); - const execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + const execCalldata = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositData)); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - console.log('Sending AllowanceHolder.exec → router.performExecution...'); + console.log('Sending AllowanceHolder.exec → router.bridge...'); const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); - logTxnSummary('Polygon USDC → Base USDC — Relay — performExecution preFee', CHAIN_IDS.POLYGON, receipt); + logTxnSummary('Polygon USDC → Base USDC — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); } main().catch((err) => { diff --git a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts index 66cbc85..d070ae0 100644 --- a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts +++ b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon USDC → Base USDC via Relay.link (no swap) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge * * Modular action sequence: @@ -10,7 +10,7 @@ * [3] call(depositTarget, depositData) — Relay bridge * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performModularExecution.preFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performActions.preFee.ts */ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; @@ -91,14 +91,14 @@ async function main(): Promise { exec.call(depositTarget, depositData); const routerIface = new ethers.Interface(ROUTER_ABI); - const execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const execCalldata = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - console.log('Sending AllowanceHolder.exec → router.performModularExecution...'); + console.log('Sending AllowanceHolder.exec → router.performActions...'); const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); - logTxnSummary('Polygon USDC → Base USDC — Relay — performModularExecution preFee', CHAIN_IDS.POLYGON, receipt); + logTxnSummary('Polygon USDC → Base USDC — Relay — performActions preFee', CHAIN_IDS.POLYGON, receipt); } main().catch((err) => { diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts index 198a451..adaf4f6 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Arbitrum USDC → ETH (OpenOcean) → Base ETH (Stargate Native ETH Pool) - * Function: performExecution (monolithic) + * Function: swapAndBridge * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap * * BRIDGE_VALUE_FLAG set: router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. @@ -33,18 +33,18 @@ import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowance import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { - MonolithicExecutionCall, BRIDGE_VALUE_FLAG, - NO_FEE, + POST_FEE_FLAG, ZERO_ADDRESS, ZERO_BYTES32, - monolithicArgs, bridgeAmountPositionFlag, + swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); +const FLAGS = POST_FEE_FLAG | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const STARGATE_ABI = [ 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', @@ -101,7 +101,7 @@ async function fetchStargateQuote( }; } -function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { +function buildStargateCalldata(nativeFee: bigint, recipient: string, amountLD: bigint): string { return STARGATE_IFACE.encodeFunctionData('send', [ { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, { nativeFee, lzTokenFee: 0n }, @@ -150,13 +150,16 @@ async function main() { await ensureRouterNativeBalance(signer, ROUTER_ARB); await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); - const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress, amountLD); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.USDC_ARB, inputAmount }, - preFee: NO_FEE, - swap: { + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: TOKENS.USDC_ARB, inputAmount }, + { receiver: signerAddress, amount: feeAmount }, + { target: ooRouter, approvalSpender: ooRouter, outputToken: NATIVE_TOKEN_ADDRESS, @@ -164,15 +167,11 @@ async function main() { minOutput: minAmountOut, returnDataWordOffset: 0n, }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer }, - flags: BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: swapData, - bridgeCallData: stargateData, - }; - - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + swapData, + { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer }, + stargateData, + ), + ); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData, nativeFeeWithBuffer); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts index 49c6a92..2b8a109 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Arbitrum USDC → ETH (OpenOcean) → Base ETH (Stargate Native ETH Pool) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap * * Modular action sequence: @@ -14,7 +14,7 @@ * bridgeValue = minAmountOut - feeAmount (amountLD + nativeFeeWithBuffer). * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performActions.postFee.ts */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -161,13 +161,13 @@ async function main() { exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(STARGATE_NATIVE_ARB, stargateData, bridgeValue); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData, nativeFeeWithBuffer); logTxnSummary( - 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performModularExecution postFee', + 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performActions postFee', CHAIN_IDS.ARBITRUM, receipt, ); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts index fa2f89d..ed16e8c 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -1,56 +1,66 @@ /** - * Route: Base USDC → ETH (OpenOcean) → Arbitrum ETH (Stargate Native ETH Pool) - * Function: performExecution (monolithic) - * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) + * Flags: post-fee (fee taken from ETH output after swap), output read from swap returndata word 0 + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate * - * BRIDGE_VALUE_FLAG set: router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. - * BRIDGE_AMOUNT_POSITION_FLAG set: router splices finalETH into amountLD at runtime. - * Stargate receives the exact actual post-swap, post-fee ETH as amountLD. + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. + * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts */ -import axios from 'axios'; -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; dotenv.config(); import { CHAIN_IDS, routerAddressForChain, TOKENS, + NATIVE_TOKEN_ADDRESS, FEE_BPS, bpsOf, RPC, OPEN_OCEAN_API_KEY, OO_SLIPPAGE_PERCENT, - NATIVE_TOKEN_ADDRESS, STARGATE_NATIVE_BASE, ARBITRUM_LZ_EID, - STARGATE_AMOUNT_LD_OFFSET, -} from '../config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { getWalletErc20Balance } from '../utils/erc20'; -import { ROUTER_ABI } from '../utils/routerAbi'; +} from "../config"; import { - MonolithicExecutionCall, + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, BRIDGE_VALUE_FLAG, - NO_FEE, ZERO_ADDRESS, - ZERO_BYTES32, - monolithicArgs, bridgeAmountPositionFlag, -} from '../utils/contractTypes'; -import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; + swapAndBridgeArgs, +} from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, + ensureRouterApproval, +} from "../utils/reproducibility"; +// post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee +const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); const STARGATE_ABI = [ - 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', - 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', - 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", ]; + const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); interface OoQuoteResponse { @@ -70,7 +80,7 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ slippage: OO_SLIPPAGE_PERCENT, sender: ROUTER_BASE, account: ROUTER_BASE, - gasPrice: '1', + gasPrice: "1", }; if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; @@ -87,21 +97,48 @@ async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ async function fetchStargateQuote( provider: ethers.JsonRpcProvider, bridgeAmountLD: bigint, - recipient: string, -): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { - const contract = new ethers.Contract(STARGATE_NATIVE_BASE, STARGATE_ABI, provider); + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_BASE, + STARGATE_ABI, + provider + ); const to32 = ethers.zeroPadValue(recipient, 32); - const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; - const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); return { - nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + nativeFee: fee.nativeFee as bigint, amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, }; } -function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { - return STARGATE_IFACE.encodeFunctionData('send', [ - { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, { nativeFee, lzTokenFee: 0n }, recipient, ]); @@ -109,79 +146,118 @@ function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: s async function main() { const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); const provider = new ethers.JsonRpcProvider(RPC.BASE); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); - const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_BASE, signerAddress, provider); - if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_BASE, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); const inputAmount = walletBalance - 20n; - if (inputAmount === 0n) throw new Error('Balance too small'); + if (inputAmount === 0n) throw new Error("Balance too small"); console.log(`Signer: ${signerAddress}`); console.log(`Router: ${ROUTER_BASE}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-value)` + ); console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); const routerIface = new ethers.Interface(ROUTER_ABI); - console.log('Fetching OpenOcean quote (USDC → ETH on Base)...'); - const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated ETH output after swap const feeAmount = bpsOf(estimatedOut, FEE_BPS); - const estimatedBridgeAmount = estimatedOut - feeAmount; console.log(` OO router: ${ooRouter}`); console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); - console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log( + ` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)` + ); console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); - console.log('Fetching Stargate quote (Base → Arbitrum native pool)...'); - const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); + const bridgeEstimate = estimatedOut - feeAmount; + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + bridgeEstimate, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); - // estimatedBridgeAmount is a placeholder; router splices the actual finalETH at runtime - const amountLD = estimatedBridgeAmount; + // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = bridgeEstimate; await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); - const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); - - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.USDC_BASE, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: NATIVE_TOKEN_ADDRESS, - value: 0n, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: STARGATE_NATIVE_BASE, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer }, - flags: BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: swapData, - bridgeCallData: stargateData, - }; + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.USDC_BASE, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ooRouter, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_BASE, + approvalSpender: ZERO_ADDRESS, + value: nativeFeeWithBuffer, + }, + stargateData, + )); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); - const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData, nativeFeeWithBuffer); + const receipt = await execViaAH( + signer, + ROUTER_BASE, + TOKENS.USDC_BASE, + inputAmount, + ROUTER_BASE, + callData, + nativeFeeWithBuffer + ); logTxnSummary( - 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performExecution postFee', + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge postFee/returndata`, CHAIN_IDS.BASE, - receipt, + receipt ); - console.log('\nETH arrives on Arbitrum once LZ delivers the message.'); + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts index d26740e..7afc9c8 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Base USDC → ETH (OpenOcean) → Arbitrum ETH (Stargate Native ETH Pool) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap * * Modular action sequence: @@ -11,7 +11,7 @@ * [4] nativeCall(Stargate, sendData, bridgeValue) — value = amountLD + nativeFeeWithBuffer * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performActions.postFee.ts */ import axios from 'axios'; import { ethers } from 'ethers'; @@ -157,13 +157,13 @@ async function main() { exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(STARGATE_NATIVE_BASE, stargateData, bridgeValue); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData, nativeFeeWithBuffer); logTxnSummary( - 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performModularExecution postFee', + 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performActions postFee', CHAIN_IDS.BASE, receipt, ); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts index b56d40e..cd5fc38 100644 --- a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon POL (native) → USDT0 (OpenOcean) → Arbitrum USDT0 (LZ OFT Adapter) - * Function: performExecution (monolithic) + * Function: swapAndBridge * Fee: postFee — FEE_BPS of estimatedOut USDT0 deducted after swap * * Input is native POL; msg.value = ooSwapNativeWei + nativeFeeWithBuffer. @@ -35,17 +35,17 @@ import { import { execViaAH } from '../utils/allowanceHolder'; import { ROUTER_ABI } from '../utils/routerAbi'; import { - MonolithicExecutionCall, - NO_FEE, + POST_FEE_FLAG, ZERO_ADDRESS, ZERO_BYTES32, bridgeAmountPositionFlag, - monolithicArgs, + swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; @@ -189,11 +189,15 @@ async function main() { const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount: inputAmountWei }, - preFee: NO_FEE, - swap: { + const txValue = inputAmountWei + nativeFeeWithBuffer; + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount: inputAmountWei }, + { receiver: signerAddress, amount: feeAmount }, + { target: ooRouter, approvalSpender: ZERO_ADDRESS, outputToken: TOKENS.USDT0_POLYGON, @@ -201,16 +205,11 @@ async function main() { minOutput: minAmountOut, returnDataWordOffset: 0n, }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, - flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: swapData, - bridgeCallData: oftSendData, - }; - - const txValue = inputAmountWei + nativeFeeWithBuffer; - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); + swapData, + { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, + oftSendData, + ), + ); // Native input — no ERC-20 allowance needed for AH; pass NATIVE_TOKEN_ADDRESS const receipt = await execViaAH(signer, ROUTER_POLYGON, NATIVE_TOKEN_ADDRESS, inputAmountWei, ROUTER_POLYGON, callData, txValue); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts index 2bd1c19..b65902f 100644 --- a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon POL (native) → USDT0 (OpenOcean) → Arbitrum USDT0 (LZ OFT Adapter) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of estimatedOut USDT0 transferred to signer after swap * * Input is native POL; msg.value = ooSwapNativeWei + nativeFeeWithBuffer. @@ -13,7 +13,7 @@ * [4] nativeCall(adapter, oftSendData, nativeFeeWithBuffer) — splicePayloadWord(196) from [3] * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonPolUsdt0Arb.performActions.postFee.ts */ import axios from 'axios'; import { ethers, parseEther } from 'ethers'; @@ -192,12 +192,12 @@ async function main() { .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); const txValue = inputAmountWei + nativeFeeWithBuffer; - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); const receipt = await execViaAH(signer, ROUTER_POLYGON, NATIVE_TOKEN_ADDRESS, inputAmountWei, ROUTER_POLYGON, callData, txValue); logTxnSummary( - 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (OFT) — performModularExecution postFee', + 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (OFT) — performActions postFee', CHAIN_IDS.POLYGON, receipt, ); diff --git a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts index 5483846..80b62be 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts @@ -5,7 +5,7 @@ * * Bridge amount is pre-encoded in Stargate send() calldata (no splice needed). * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. - * Uses router.bridge() rather than performExecution / performModularExecution. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts diff --git a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts index 50c0083..b4256dc 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -1,9 +1,11 @@ /** * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) - * Function: performExecution (monolithic) - * Fee: postFee — FEE_BPS of inputAmount USDC deducted; bridge amount position flag splices - * actual post-fee balance into amountLD at byte 196 of Stargate send() calldata. + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in Stargate send() calldata (no splice needed). * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performActions. * * Usage: * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -21,19 +23,11 @@ import { RPC, STARGATE_USDC_POLYGON, BASE_LZ_EID, - STARGATE_AMOUNT_LD_OFFSET, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; -import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, -} from '../utils/contractTypes'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; @@ -53,83 +47,112 @@ async function fetchStargateQuote( ): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); const to32 = ethers.zeroPadValue(recipient, 32); - const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; - const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); return { nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, }; } -function buildStargateCalldata(nativeFee: bigint, recipient: string): string { - // amountLD = 0 placeholder; router splices actual balance at STARGATE_AMOUNT_LD_OFFSET +/** + * Builds Stargate send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildStargateCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { return STARGATE_IFACE.encodeFunctionData('send', [ - { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }, { nativeFee, lzTokenFee: 0n }, recipient, ]); } -async function main() { +async function main(): Promise { const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } const provider = new ethers.JsonRpcProvider(RPC.POLYGON); const signer = new ethers.Wallet(privateKey, provider); const signerAddress = await signer.getAddress(); - const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); - if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + } const inputAmount = walletBalance - 20n; if (inputAmount === 0n) throw new Error('Balance too small'); const feeAmount = bpsOf(inputAmount, FEE_BPS); - const estimatedBridgeAmount = inputAmount - feeAmount; + const bridgeAmount = inputAmount - feeAmount; - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); - console.log(`Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); - console.log(`Est. bridge: ${ethers.formatUnits(estimatedBridgeAmount, 6)} USDC`); - - const routerIface = new ethers.Interface(ROUTER_ABI); + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); - const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); - console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); - await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, STARGATE_USDC_POLYGON); - - const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress); + const sendData = buildStargateCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); - const mono: MonolithicExecutionCall = { - exec: { - input: { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }, - preFee: NO_FEE, - swap: NO_SWAP, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { target: STARGATE_USDC_POLYGON, approvalSpender: STARGATE_USDC_POLYGON, value: nativeFeeWithBuffer }, - flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: '0x', - bridgeCallData: stargateData, + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { + target: STARGATE_USDC_POLYGON, + approvalSpender: STARGATE_USDC_POLYGON, + value: nativeFeeWithBuffer, }; - const callData = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono, ZERO_BYTES32)); - - await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); - const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); - - logTxnSummary( - 'Polygon USDC → Base USDC (Stargate USDC pool) — performExecution postFee', - CHAIN_IDS.POLYGON, - receipt, + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, STARGATE_USDC_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, ); + logTxnSummary('Polygon USDC → Base USDC (Stargate USDC pool) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + console.log('\nUSDC arrives on Base once LZ delivers the message.'); } -main().catch((err) => { console.error(err); process.exit(1); }); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts index 7ecf31e..1a5e457 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts @@ -1,6 +1,6 @@ /** * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) - * Function: performModularExecution (modular) + * Function: performActions (modular) * Fee: postFee — FEE_BPS of inputAmount USDC transferred to signer; staticCall balance spliced * into Stargate amountLD at STARGATE_AMOUNT_LD_OFFSET (byte 196). * @@ -12,7 +12,7 @@ * [4] nativeCall(Stargate, sendData, nativeFeeWithBuffer) — splicePayloadWord(196) from [3] * * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performActions.postFee.ts */ import { ethers } from 'ethers'; import * as dotenv from 'dotenv'; @@ -116,13 +116,13 @@ async function main() { exec.nativeCall(STARGATE_USDC_POLYGON, stargateData, nativeFeeWithBuffer) .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); - const callData = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, exec.toActions()]); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); logTxnSummary( - 'Polygon USDC → Base USDC (Stargate USDC pool) — performModularExecution postFee', + 'Polygon USDC → Base USDC (Stargate USDC pool) — performActions postFee', CHAIN_IDS.POLYGON, receipt, ); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts index 46c549f..0b3a295 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -41,6 +41,7 @@ import { BRIDGE_VALUE_FLAG, ZERO_ADDRESS, bridgeAmountPositionFlag, + swapAndBridgeArgs, } from "../utils/contractTypes"; import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; @@ -209,14 +210,14 @@ async function main() { amountLD ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -233,7 +234,7 @@ async function main() { value: nativeFeeWithBuffer, }, stargateData, - ]); + )); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts index 6ccdd7e..10f84e2 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -41,6 +41,7 @@ import { BRIDGE_VALUE_FLAG, ZERO_ADDRESS, bridgeAmountPositionFlag, + swapAndBridgeArgs, } from "../utils/contractTypes"; import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; @@ -210,14 +211,14 @@ async function main() { amountLD ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -234,7 +235,7 @@ async function main() { value: nativeFeeWithBuffer, }, stargateData, - ]); + )); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts index 8addf9b..00e5659 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -41,6 +41,7 @@ import { BRIDGE_VALUE_FLAG, ZERO_ADDRESS, bridgeAmountPositionFlag, + swapAndBridgeArgs, } from "../utils/contractTypes"; import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; @@ -209,14 +210,14 @@ async function main() { amountLD ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -233,7 +234,7 @@ async function main() { value: nativeFeeWithBuffer, }, stargateData, - ]); + )); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts index 0a2ee3a..2324ea5 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -41,6 +41,7 @@ import { BRIDGE_VALUE_FLAG, ZERO_ADDRESS, bridgeAmountPositionFlag, + swapAndBridgeArgs, } from "../utils/contractTypes"; import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; import { logTxnSummary } from "../utils/txnLogSummary"; @@ -209,14 +210,14 @@ async function main() { amountLD ); - const callData = routerIface.encodeFunctionData("swapAndBridge", [ + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.USDC_BASE, inputAmount: inputAmount, }, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -233,7 +234,7 @@ async function main() { value: nativeFeeWithBuffer, }, stargateData, - ]); + )); await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); const receipt = await execViaAH( diff --git a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts index cc4ccb4..570c08e 100644 --- a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts @@ -33,7 +33,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -183,15 +183,14 @@ async function main() { ksRouter, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, @@ -202,7 +201,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/kyberswap.postFee.returndata.ts b/scripts/e2e/swap/kyberswap.postFee.returndata.ts index 06e35dd..6da3af8 100644 --- a/scripts/e2e/swap/kyberswap.postFee.returndata.ts +++ b/scripts/e2e/swap/kyberswap.postFee.returndata.ts @@ -34,7 +34,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -178,15 +178,14 @@ async function main() { ksRouter, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, @@ -197,7 +196,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts index 1043d72..9f9312d 100644 --- a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts @@ -35,7 +35,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -189,15 +189,14 @@ async function main() { ksRouter, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, @@ -208,7 +207,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/kyberswap.preFee.returndata.ts b/scripts/e2e/swap/kyberswap.preFee.returndata.ts index 95df823..59c9302 100644 --- a/scripts/e2e/swap/kyberswap.preFee.returndata.ts +++ b/scripts/e2e/swap/kyberswap.preFee.returndata.ts @@ -36,7 +36,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -185,15 +185,14 @@ async function main() { ksRouter, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, @@ -204,7 +203,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/swap.postFee.balanceOf.ts b/scripts/e2e/swap/swap.postFee.balanceOf.ts index 7faf26d..06e8377 100644 --- a/scripts/e2e/swap/swap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.postFee.balanceOf.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -125,15 +125,14 @@ async function main() { ooRouter ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -144,7 +143,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/swap.postFee.returndata.ts b/scripts/e2e/swap/swap.postFee.returndata.ts index 01eb4b7..9fa27fa 100644 --- a/scripts/e2e/swap/swap.postFee.returndata.ts +++ b/scripts/e2e/swap/swap.postFee.returndata.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -120,15 +120,14 @@ async function main() { ooRouter ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -139,7 +138,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/swap.preFee.balanceOf.ts b/scripts/e2e/swap/swap.preFee.balanceOf.ts index d21c98b..4e4c5c0 100644 --- a/scripts/e2e/swap/swap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.preFee.balanceOf.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -125,15 +125,14 @@ async function main() { ooRouter ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -144,7 +143,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/swap.preFee.returndata.ts b/scripts/e2e/swap/swap.preFee.returndata.ts index 7de1155..bd38078 100644 --- a/scripts/e2e/swap/swap.preFee.returndata.ts +++ b/scripts/e2e/swap/swap.preFee.returndata.ts @@ -31,7 +31,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -120,15 +120,14 @@ async function main() { ooRouter ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, @@ -139,7 +138,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/zerox.postFee.balanceOf.ts b/scripts/e2e/swap/zerox.postFee.balanceOf.ts index 593ef82..0e1e7e0 100644 --- a/scripts/e2e/swap/zerox.postFee.balanceOf.ts +++ b/scripts/e2e/swap/zerox.postFee.balanceOf.ts @@ -36,7 +36,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -179,15 +179,14 @@ async function main() { approvalSpender, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, @@ -198,7 +197,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/zerox.postFee.returndata.ts b/scripts/e2e/swap/zerox.postFee.returndata.ts index 3f88890..12253ef 100644 --- a/scripts/e2e/swap/zerox.postFee.returndata.ts +++ b/scripts/e2e/swap/zerox.postFee.returndata.ts @@ -35,7 +35,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -168,15 +168,14 @@ async function main() { approvalSpender, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, @@ -187,7 +186,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/zerox.preFee.balanceOf.ts b/scripts/e2e/swap/zerox.preFee.balanceOf.ts index ca37956..75b18a1 100644 --- a/scripts/e2e/swap/zerox.preFee.balanceOf.ts +++ b/scripts/e2e/swap/zerox.preFee.balanceOf.ts @@ -39,7 +39,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -186,15 +186,14 @@ async function main() { approvalSpender, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, @@ -205,7 +204,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swap/zerox.preFee.returndata.ts b/scripts/e2e/swap/zerox.preFee.returndata.ts index 2ca1b56..dfa9057 100644 --- a/scripts/e2e/swap/zerox.preFee.returndata.ts +++ b/scripts/e2e/swap/zerox.preFee.returndata.ts @@ -37,7 +37,7 @@ import { } from "../utils/allowanceHolder"; import { getWalletErc20Balance } from "../utils/erc20"; import { ROUTER_ABI } from "../utils/routerAbi"; -import { ZERO_BYTES32 } from "../utils/contractTypes"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, @@ -178,15 +178,14 @@ async function main() { approvalSpender, ); - const callData = routerIface.encodeFunctionData("swap", [ + const callData = routerIface.encodeFunctionData("swap", swapArgs( ZERO_BYTES32, + FLAGS, { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount: inputAmount, }, - signerAddress, - FLAGS, { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, @@ -197,7 +196,8 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - ]); + signerAddress, + )); await ensureAllowanceForAllowanceHolder( signer, diff --git a/scripts/e2e/swapBridgeViaArbitrumNative.ts b/scripts/e2e/swapBridgeViaArbitrumNative.ts deleted file mode 100644 index 1dce431..0000000 --- a/scripts/e2e/swapBridgeViaArbitrumNative.ts +++ /dev/null @@ -1,435 +0,0 @@ -/** - * Arbitrum bridge e2e script — AAVE (Ethereum) → ETH (OO swap) → Arbitrum ETH (depositEth) - * - * Flow: - * 1. Fetch an OpenOcean swap quote for AAVE → ETH on Ethereum mainnet (router is sender). - * 2. Estimate the Arbitrum retryable submission fee so we know the minimum ETH required - * to bridge. A conservative fallback of 0.001 ETH is used if estimation fails. - * 3. Split the signer's AAVE balance in half and run two legs back-to-back: - * Leg 1 MONOLITHIC — single `performExecution` call - * Leg 2 MODULAR — `performModularExecution` call (3-second pause before) - * - * Monolithic mechanics: - * - Pull inputAmount AAVE via AH.exec grant, approve OO router, swap AAVE → ETH. - * - Post-swap fee (FEE_BPS) in ETH sent to signer. - * - BRIDGE_VALUE_FLAG set: router forwards actualFinalETH as msg.value to inbox. - * - No ETH splice needed (depositEth takes no calldata amount param). - * - * Modular mechanics: - * [0] AH.transferFrom AAVE → router (uses ephemeral AH grant) - * [1] AAVE.approve(ooRouter, inputAmount) - * [2] OO swap AAVE → ETH (lands in router) - * [3] nativeCall(signer, '0x', feeAmount) — ETH fee out - * [4] nativeCall(inbox, depositEth(), bridgeValue) - * - * Input is always AAVE (ERC-20) so `direct` router txs are rejected — the router's - * `_pullFromUser` requires the ephemeral allowance set by AllowanceHolder.exec. - * - * Exec mode (argv[1] or ARB_ROUTER_EXEC env): - * allowance-holder (default) — wrap via AllowanceHolder.exec - * direct — rejected for ERC-20 input with a clear error - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts allowance-holder - * ARB_ROUTER_EXEC=allowance-holder PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaArbitrumNative.ts - * - * Router on Ethereum mainnet: set `ROUTER_CHAIN_1` env or legacy `ROUTER_ADDRESS`. - */ -import axios from 'axios'; -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - ARBITRUM_INBOX, - FEE_BPS, - bpsOf, - RPC, - OPEN_OCEAN_API_KEY, - OO_SLIPPAGE_PERCENT, - ALLOWANCE_HOLDER, - NATIVE_TOKEN_ADDRESS, -} from './config'; -import { - execViaAH, - execDirect, - ensureAllowanceForAllowanceHolder, -} from './utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; -import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - BRIDGE_VALUE_FLAG, - MonolithicExecutionCall, - NO_FEE, - ZERO_ADDRESS, - ZERO_BYTES32, - monolithicArgs, -} from './utils/contractTypes'; -import { sleep } from './utils/sleep'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterNativeBalance, - ensureRouterApproval, -} from './utils/reproducibility'; - -// ─── Exec-mode selection ────────────────────────────────────────────────────── - -/** How the signer reaches the router. */ -type RouterExecRoute = 'direct' | 'allowance-holder'; - -const EXEC_ALIASES: Record = { - direct: 'direct', - dr: 'direct', - router: 'direct', - 'allowance-holder': 'allowance-holder', - ah: 'allowance-holder', - exec: 'allowance-holder', -}; - -/** - * Resolves exec route from `argv[1]` (overrides) or `ARB_ROUTER_EXEC` env. - * Defaults to `allowance-holder` since AAVE is ERC-20. - * `direct` is rejected with a clear error because `_pullFromUser` requires AH. - */ -function resolveRouterExecRoute(): RouterExecRoute { - const rawArg = typeof process.argv[2] === 'string' ? process.argv[2].trim().toLowerCase() : ''; - const rawEnv = (process.env.ARB_ROUTER_EXEC ?? '').trim().toLowerCase(); - const raw = rawArg || rawEnv; - - if (raw) { - const route = EXEC_ALIASES[raw]; - if (!route) { - console.error( - `Unknown exec mode "${raw}". Use argv[1] or ARB_ROUTER_EXEC: allowance-holder | direct (aliases ah, exec, dr, router).`, - ); - process.exit(1); - } - if (route === 'direct') { - console.error( - 'ERC-20 input (AAVE) cannot use direct router txs: `_pullFromUser` invokes AllowanceHolder.transferFrom, ' + - 'which requires the ephemeral allowance set by AH.exec. Use allowance-holder (default).', - ); - process.exit(1); - } - return route; - } - - return 'allowance-holder'; -} - -// ─── Arbitrum bridge fee estimation ────────────────────────────────────────── - -/** - * Estimates the minimum ETH required for the Arbitrum inbox submission fee. - * Falls back to 0.001 ETH if the SDK is unavailable or estimation fails. - */ -async function estimateArbitrumBridgeFee(ethereumProvider: ethers.Provider): Promise { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); - const estimator = new ParentToChildMessageGasEstimator(ethereumProvider); - const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; - const submissionFee = await estimator.estimateSubmissionFee(ethereumProvider, 0n, 0n); - const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); - const totalFee = BigInt(submissionFee.toString()) + executionCost; - console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); - return totalFee; - } catch (err) { - const fallback = ethers.parseEther('0.001'); - console.warn( - ` Arbitrum fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`, - ); - return fallback; - } -} - -// ─── OpenOcean quote ────────────────────────────────────────────────────────── - -interface OoSwapQuoteResponse { - data: { - to: string; - data: string; - value?: string; - outAmount: string; - minOutAmount: string; - }; -} - -/** - * Fetches an OpenOcean swap_quote for AAVE → ETH on Ethereum mainnet. - * Router address is used as sender and account so ETH output lands in the router. - */ -async function fetchOoQuote( - routerAddress: string, - inputAmount: bigint, -): Promise<{ - ooRouter: string; - swapData: string; - estimatedOut: bigint; - minAmountOut: bigint; -}> { - const params: Record = { - inTokenAddress: TOKENS.AAVE_ETH, - outTokenAddress: NATIVE_TOKEN_ADDRESS, - amount: ethers.formatUnits(inputAmount, 18), - slippage: OO_SLIPPAGE_PERCENT, - sender: routerAddress, - account: routerAddress, - gasPrice: '20', - }; - if (OPEN_OCEAN_API_KEY) { - params.apikey = OPEN_OCEAN_API_KEY; - } - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; - const response = await axios.get(url, { params }); - const q = response.data.data; - return { - ooRouter: q.to, - swapData: q.data, - estimatedOut: BigInt(q.outAmount), - minAmountOut: BigInt(q.minOutAmount), - }; -} - -// ─── Calldata helpers ───────────────────────────────────────────────────────── - -/** Encodes Arbitrum inbox `depositEth()` — ETH amount is entirely in msg.value. */ -function buildDepositEthCalldata(): string { - return new ethers.Interface([ - 'function depositEth() external payable returns (uint256)', - ]).encodeFunctionData('depositEth', []); -} - -// ─── Monolithic builder ─────────────────────────────────────────────────────── - -/** - * AAVE → OO → ETH → Arbitrum inbox (monolithic): - * - input: AAVE pulled via AH - * - swap: AAVE → native ETH, BRIDGE_VALUE_FLAG forwards actualFinalETH - * - bridge: depositEth() — no amount in calldata, all ETH passed as msg.value - */ -function buildMonolithic( - signerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - minAmountOut: bigint, - ooRouter: string, - swapData: string, -): MonolithicExecutionCall { - return { - exec: { - input: { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: NATIVE_TOKEN_ADDRESS, - value: 0n, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signerAddress, amount: feeAmount }, - bridge: { - target: ARBITRUM_INBOX, - approvalSpender: ZERO_ADDRESS, - value: 0n, // no addend: bridgeValue = finalETH + 0 = finalETH - }, - flags: BRIDGE_VALUE_FLAG, - }, - swapCallData: swapData, - bridgeCallData: buildDepositEthCalldata(), - }; -} - -// ─── Modular builder ────────────────────────────────────────────────────────── - -/** - * AAVE → OO → ETH → Arbitrum inbox (modular): - * [0] AH.transferFrom(AAVE, signer, router, inputAmount) - * [1] AAVE.approve(ooRouter, inputAmount) - * [2] call(ooRouter, swapData) — AAVE → ETH, ETH lands in router - * [3] nativeCall(signer, '0x', feeAmount) - * [4] nativeCall(inbox, depositEthData, bridgeValue) - */ -function buildModularActions( - signerAddress: string, - routerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - bridgeValue: bigint, - ooRouter: string, - swapData: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const exec = new ModularActionsBuilder(); - - exec.call( - ALLOWANCE_HOLDER, - ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_ETH, signerAddress, routerAddress, inputAmount]), - ); - exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouter, inputAmount)); - exec.call(ooRouter, swapData); - exec.nativeCall(signerAddress, '0x', feeAmount); - exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); - - return exec.toActions(); -} - -// ─── Execution leg ──────────────────────────────────────────────────────────── - -/** - * Runs one monolithic or modular leg: fetches OO quote + arb fee, builds calldata, - * dispatches via AllowanceHolder.exec (msg.value=0 since input is AAVE). - */ -async function executeLeg( - legLabel: string, - useModular: boolean, - routerAddress: string, - signer: ethers.Wallet, - signerAddress: string, - provider: ethers.JsonRpcProvider, - inputAmount: bigint, - routerExec: RouterExecRoute, - routerIface: ethers.Interface, -): Promise { - console.log(`\n── ${legLabel} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - - console.log('Fetching OpenOcean quote (AAVE → ETH)...'); - const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOoQuote( - routerAddress, - inputAmount, - ); - const feeAmount = bpsOf(estimatedOut, FEE_BPS); - - console.log(` OO router: ${ooRouter}`); - console.log(` Est. ETH out: ${ethers.formatEther(estimatedOut)} ETH`); - console.log(` Fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); - console.log(` Min ETH out: ${ethers.formatEther(minAmountOut)} ETH`); - - await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, routerAddress); - await ensureRouterNativeBalance(signer, routerAddress); - await ensureRouterApproval(signer, routerAddress, TOKENS.AAVE_ETH, ooRouter); - - const arbFee = await estimateArbitrumBridgeFee(provider); - const minEthRequired = feeAmount + arbFee; - if (estimatedOut < minEthRequired) { - console.warn( - ` Warning: est. ETH out (${ethers.formatEther(estimatedOut)}) may be insufficient ` + - `to cover fee + bridge cost (${ethers.formatEther(minEthRequired)}).`, - ); - } - - // bridgeValue = everything left after the fee; use minAmountOut-based floor so - // the modular nativeCall carries at least as much ETH as the inbox requires. - const bridgeValue = minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n; - console.log(` Bridge value: ${ethers.formatEther(bridgeValue)} ETH (floor for nativeCall)`); - - let execCalldata: string; - if (useModular) { - const actions = buildModularActions( - signerAddress, - routerAddress, - inputAmount, - feeAmount, - bridgeValue, - ooRouter, - swapData, - ); - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, actions]); - } else { - const mono = buildMonolithic(signerAddress, inputAmount, feeAmount, minAmountOut, ooRouter, swapData); - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono)); - } - - // Input is AAVE (ERC-20) — msg.value is always 0; ETH comes from the swap output. - const txValue = 0n; - - let receipt: ethers.TransactionReceipt; - if (routerExec === 'direct') { - // Guarded at startup — should never reach here for ERC-20 input. - console.log(`[exec=direct] value=0 ETH`); - receipt = await execDirect(signer, routerAddress, execCalldata, txValue); - } else { - await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); - console.log(`[exec=allowance-holder] value=0 ETH`); - receipt = await execViaAH( - signer, - routerAddress, - TOKENS.AAVE_ETH, - inputAmount, - routerAddress, - execCalldata, - txValue, - ); - } - - logTxnSummary( - `AAVE → ETH → Arbitrum — ${useModular ? 'Modular' : 'Monolithic'}`, - CHAIN_IDS.ETHEREUM, - receipt, - ); -} - -// ─── Main ───────────────────────────────────────────────────────────────────── - -async function main(): Promise { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const routerExec = resolveRouterExecRoute(); - const routerAddress = routerAddressForChain(CHAIN_IDS.ETHEREUM); - - const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const { balance: fullBalance, decimals } = await getWalletErc20Balance( - TOKENS.AAVE_ETH, - signerAddress, - provider, - ); - if (fullBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero AAVE on Ethereum. Fund the wallet first.`, - ); - } - - const legAmount = (fullBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error('AAVE balance too small to split into two legs.'); - } - - const routerIface = new ethers.Interface(ROUTER_ABI); - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${routerAddress}`); - console.log(`Input token: ${TOKENS.AAVE_ETH} (AAVE Ethereum)`); - console.log(`Balance: ${ethers.formatUnits(fullBalance, decimals)} AAVE`); - console.log(`Per leg (½): ${ethers.formatUnits(legAmount, decimals)} AAVE`); - console.log(`Exec route: ${routerExec}`); - - await executeLeg('1/2', false, routerAddress, signer, signerAddress, provider, legAmount, routerExec, routerIface); - - console.log('\nSleeping 3s before modular leg...'); - await sleep(3000); - - await executeLeg('2/2', true, routerAddress, signer, signerAddress, provider, legAmount, routerExec, routerIface); - - console.log('\n✓ Arbitrum bridge case completed.'); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/swapBridgeViaCctp.ts b/scripts/e2e/swapBridgeViaCctp.ts deleted file mode 100644 index c55af37..0000000 --- a/scripts/e2e/swapBridgeViaCctp.ts +++ /dev/null @@ -1,556 +0,0 @@ -/** - * Script 2 — Swap AAVE→USDC on Polygon, then bridge USDC to Base via CCTP v2 - * - * OpenOcean must output Circle’s **native** Polygon USDC (`USDC_POLYGON_CIRCLE`). - * Bridged USDC (`0x2791…`, USDC.e) is rejected by TokenMessenger (“Burn token not supported”). - * - * Each run uses half of the initial AAVE snapshot: monolithic then modular. - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts - * Polygon native USDC → Base USDC via CCTP only (no OpenOcean swap): - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctp.ts usdc-polygon-base - * - * Router on Polygon: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(137)`. - */ -import axios from 'axios'; -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - CCTP_CONFIG, - FEE_BPS, - bpsOf, - RPC, - OPEN_OCEAN_API_KEY, - OO_SLIPPAGE_PERCENT, - ALLOWANCE_HOLDER, -} from './config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; -import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, -} from './utils/contractTypes'; -import { sleep } from './utils/sleep'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterApproval, -} from './utils/reproducibility'; - -const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); - -interface OpenOceanSwapQuoteResponse { - data: { - to: string; - data: string; - value: string; - estimatedGas: string; - outAmount: string; - minOutAmount: string; - }; -} - -async function fetchOpenOceanSwapQuote( - routerAddress: string, - inputAmount: bigint, -): Promise<{ - routerAddress: string; - swapData: string; - minAmountOut: bigint; - estimatedOut: bigint; -}> { - const params: Record = { - inTokenAddress: TOKENS.AAVE_POLYGON, - outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, - amount: ethers.formatUnits(inputAmount, 18), - slippage: OO_SLIPPAGE_PERCENT, - sender: routerAddress, - account: routerAddress, - gasPrice: '1', - }; - if (OPEN_OCEAN_API_KEY) { - params.apikey = OPEN_OCEAN_API_KEY; - } - - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; - const response = await axios.get(url, { params }); - const q = response.data.data; - - return { - routerAddress: q.to, - swapData: q.data, - minAmountOut: BigInt(q.minOutAmount), - estimatedOut: BigInt(q.outAmount), - }; -} - -function buildDepositForBurnCalldata( - recipientAddress: string, - burnToken: string, - destinationCctpDomain: number, - fastPath: boolean = true, -): string { - const iface = new ethers.Interface([ - 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', - ]); - - const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); - const maxFee = fastPath ? 1_000_000n : 0n; - const minFinalityThreshold = fastPath ? 1000 : 2000; - - return iface.encodeFunctionData('depositForBurn', [ - 0n, - destinationCctpDomain, - mintRecipient, - burnToken, - ethers.ZeroHash, - maxFee, - minFinalityThreshold, - ]); -} - -function buildMonolithicExecution( - signerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - minAmountOut: bigint, - ooRouterAddress: string, - swapData: string, - depositForBurnData: string, - tokenMessenger: string, -): MonolithicExecutionCall { - return { - exec: { - input: { - user: signerAddress, - inputToken: TOKENS.AAVE_POLYGON, - inputAmount, - }, - preFee: NO_FEE, - swap: { - target: ooRouterAddress, - approvalSpender: ooRouterAddress, - outputToken: TOKENS.USDC_POLYGON_CIRCLE, - value: 0n, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { - receiver: signerAddress, - amount: feeAmount, - }, - bridge: { - target: tokenMessenger, - approvalSpender: tokenMessenger, - value: 0n, - }, - flags: bridgeAmountPositionFlag(4n), - }, - swapCallData: swapData, - bridgeCallData: depositForBurnData, - }; -} - -function buildModularActions( - signerAddress: string, - routerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - ooRouterAddress: string, - swapData: string, - depositForBurnData: string, - tokenMessenger: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.AAVE_POLYGON, - signerAddress, - routerAddress, - inputAmount, - ]); - - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouterAddress, inputAmount)); - exec.call(ooRouterAddress, swapData); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(tokenMessenger, ethers.MaxUint256)); - const balance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(routerAddress)); - exec.call(tokenMessenger, depositForBurnData).spliceArg(0, balance.returnWord()); - return exec.toActions(); -} - -function buildMonolithicExecutionUsdcPolygonToBaseCctp( - signerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - depositForBurnData: string, - tokenMessenger: string, -): MonolithicExecutionCall { - return { - exec: { - input: { - user: signerAddress, - inputToken: TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - }, - preFee: { - receiver: signerAddress, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: tokenMessenger, - approvalSpender: tokenMessenger, - value: 0n, - }, - flags: bridgeAmountPositionFlag(4n), - }, - swapCallData: '0x', - bridgeCallData: depositForBurnData, - }; -} - -function buildModularActionsUsdcPolygonToBaseCctp( - signerAddress: string, - routerAddress: string, - inputAmount: bigint, - feeAmount: bigint, - depositForBurnData: string, - tokenMessenger: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.USDC_POLYGON_CIRCLE, - signerAddress, - routerAddress, - inputAmount, - ]); - - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(tokenMessenger, ethers.MaxUint256)); - const balance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(routerAddress)); - exec.call(tokenMessenger, depositForBurnData).spliceArg(0, balance.returnWord()); - return exec.toActions(); -} - -async function executeLegUsdcPolygonToBaseCctp(args: { - label: string; - useModular: boolean; - signer: ethers.Wallet; - signerAddress: string; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { label, useModular, signer, signerAddress, inputAmount, routerIface } = args; - - console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - - const feeAmount = bpsOf(inputAmount, FEE_BPS); - console.log(`Input USDC: ${ethers.formatUnits(inputAmount, 6)}`); - console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); - console.log(`Net to bridge: ${ethers.formatUnits(inputAmount - feeAmount, 6)}`); - - const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; - const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; - console.log(`CCTP burn token: ${polyCctp.usdcAddress}`); - const depositForBurnData = buildDepositForBurnCalldata( - signerAddress, - polyCctp.usdcAddress, - baseCctp.cctpDomain, - true, - ); - - await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); - - let execCalldata: string; - if (useModular) { - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - buildModularActionsUsdcPolygonToBaseCctp( - signerAddress, - ROUTER_POLYGON, - inputAmount, - feeAmount, - depositForBurnData, - polyCctp.tokenMessenger, - ), - ]); - } else { - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( - buildMonolithicExecutionUsdcPolygonToBaseCctp( - signerAddress, - inputAmount, - feeAmount, - depositForBurnData, - polyCctp.tokenMessenger, - ), - )); - } - - await ensureAllowanceForAllowanceHolder( - signer, - TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - ); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - TOKENS.USDC_POLYGON_CIRCLE, - inputAmount, - ROUTER_POLYGON, - execCalldata, - ); - - const modeLabel = useModular ? 'Modular' : 'Monolithic'; - logTxnSummary( - `Polygon USDC → Base USDC — CCTP — ${modeLabel}`, - CHAIN_IDS.POLYGON, - receipt, - ); -} - -async function executeLeg(args: { - label: string; - useModular: boolean; - signer: ethers.Wallet; - signerAddress: string; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { label, useModular, signer, signerAddress, inputAmount, routerIface } = args; - - console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - - console.log('Fetching OpenOcean swap quote (Polygon AAVE → Circle native USDC)...'); - const { - routerAddress: ooRouterAddress, - swapData, - minAmountOut, - estimatedOut, - } = await fetchOpenOceanSwapQuote(ROUTER_POLYGON, inputAmount); - - const feeAmount = bpsOf(estimatedOut, FEE_BPS); - console.log(`OO Router: ${ooRouterAddress}`); - console.log(`Est. USDC out: ${ethers.formatUnits(estimatedOut, 6)}`); - console.log(`Post-swap fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); - console.log(`Min USDC out: ${ethers.formatUnits(minAmountOut, 6)}`); - - const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; - const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; - console.log(`CCTP burn token: ${polyCctp.usdcAddress} (must match swap output)`); - const depositForBurnData = buildDepositForBurnCalldata( - signerAddress, - polyCctp.usdcAddress, - baseCctp.cctpDomain, - true, - ); - - await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouterAddress); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); - - let execCalldata: string; - if (useModular) { - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - buildModularActions( - signerAddress, - ROUTER_POLYGON, - inputAmount, - feeAmount, - ooRouterAddress, - swapData, - depositForBurnData, - polyCctp.tokenMessenger, - ), - ]); - } else { - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( - buildMonolithicExecution( - signerAddress, - inputAmount, - feeAmount, - minAmountOut, - ooRouterAddress, - swapData, - depositForBurnData, - polyCctp.tokenMessenger, - ), - )); - } - - await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - TOKENS.AAVE_POLYGON, - inputAmount, - ROUTER_POLYGON, - execCalldata, - ); - - const modeLabel = useModular ? 'Modular' : 'Monolithic'; - logTxnSummary( - `Polygon AAVE → Base USDC — OpenOcean + CCTP — ${modeLabel}`, - CHAIN_IDS.POLYGON, - receipt, - ); -} - -async function mainUsdcPolygonToBaseCctp() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.USDC_POLYGON_CIRCLE; - const { balance: walletBalance } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero Circle native USDC on Polygon. Fund ${inputToken} on Polygon PoS.`, - ); - } - - const legAmount = (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error( - `Balance ${walletBalance} too small for two nonzero 50% legs.`, - ); - } - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`Polygon USDC: ${ethers.formatUnits(walletBalance, 6)} (full)`); - console.log(`Per leg input: ${ethers.formatUnits(legAmount, 6)} (50%)`); - - const routerIface = new ethers.Interface(ROUTER_ABI); - - await executeLegUsdcPolygonToBaseCctp({ - label: '1/2 Monolithic', - useModular: false, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log('Sleeping ~3s before modular execution...'); - await sleep(3000); - - await executeLegUsdcPolygonToBaseCctp({ - label: '2/2 Modular', - useModular: true, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log( - `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`, - ); -} - -async function main() { - const cctpE2eCase = process.argv[2]?.toLowerCase(); - if (cctpE2eCase === 'usdc-polygon-base' || cctpE2eCase === 'usdc') { - await mainUsdcPolygonToBaseCctp(); - return; - } - - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.AAVE_POLYGON; - const { balance: walletBalance } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero Polygon AAVE. Fund ${inputToken} on Polygon PoS.`, - ); - } - - const legAmount = (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error( - `Balance ${walletBalance} too small for two nonzero 50% legs.`, - ); - } - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`Polygon AAVE: ${ethers.formatUnits(walletBalance, 18)} (full)`); - console.log(`Per leg input: ${ethers.formatUnits(legAmount, 18)} (50%)`); - - const routerIface = new ethers.Interface(ROUTER_ABI); - - await executeLeg({ - label: '1/2 Monolithic', - useModular: false, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log('Sleeping ~3s before modular execution...'); - await sleep(3000); - - await executeLeg({ - label: '2/2 Modular', - useModular: true, - signer, - signerAddress, - inputAmount: legAmount, - routerIface, - }); - - console.log( - `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`, - ); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/swapBridgeViaCctpSimple.ts b/scripts/e2e/swapBridgeViaCctpSimple.ts deleted file mode 100644 index 5ffc4fb..0000000 --- a/scripts/e2e/swapBridgeViaCctpSimple.ts +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Polygon native USDC → Base USDC via CCTP v2 using `router.bridge(...)`. - * - * Same burn token / TokenMessenger constraints as {@link swapBridgeViaCctp}: - * use Circle’s native Polygon USDC (`USDC_POLYGON_CIRCLE`); bridged USDC.e is unsupported. - * - * Unlike the monolithic/modular paths in `swapBridgeViaCctp.ts`, this script: - * – only supports USDC-in (no OpenOcean AAVE→USDC swap); - * – encodes the net `depositForBurn` amount in calldata up front (no splice); - * – uses a single `bridge` entrypoint per run (full wallet balance by default). - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctpSimple.ts - * - * No pre-bridge fee: - * FEE_AMOUNT_BPS=0 PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaCctpSimple.ts - * - * Router: {@link ROUTER_BY_CHAIN_ID} / `routerAddressForChain(137)` in config.ts. - */ -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - CCTP_CONFIG, - FEE_BPS, - bpsOf, - RPC, -} from './config'; -import { execViaAH, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; -import { getWalletErc20Balance } from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from './utils/contractTypes'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterApproval, -} from './utils/reproducibility'; - -const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); - -/** - * CCTP `depositForBurn` with explicit burn amount (net after optional fee). - */ -function buildDepositForBurnCalldata( - recipientAddress: string, - burnToken: string, - destinationCctpDomain: number, - amount: bigint, - fastPath: boolean = true, -): string { - const iface = new ethers.Interface([ - 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', - ]); - - const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); - const maxFee = fastPath ? 1_000_000n : 0n; - const minFinalityThreshold = fastPath ? 1000 : 2000; - - return iface.encodeFunctionData('depositForBurn', [ - amount, - destinationCctpDomain, - mintRecipient, - burnToken, - ethers.ZeroHash, - maxFee, - minFinalityThreshold, - ]); -} - -function buildBridgeCalldata( - routerIface: ethers.Interface, - args: { - signerAddress: string; - inputToken: string; - inputAmount: bigint; - fee: FeeData; - tokenMessenger: string; - depositData: string; - }, -): string { - const input: InputData = { - user: args.signerAddress, - inputToken: args.inputToken, - inputAmount: args.inputAmount, - }; - - const bridgeData: BridgeData = { - target: args.tokenMessenger, - approvalSpender: args.tokenMessenger, - value: 0n, - }; - - return routerIface.encodeFunctionData('bridge', [ - ZERO_BYTES32, - input, - args.fee, - bridgeData, - args.depositData, - ]); -} - -async function main(): Promise { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = await signer.getAddress(); - - const inputToken = TOKENS.USDC_POLYGON_CIRCLE; - const { balance: walletBalance } = await getWalletErc20Balance( - inputToken, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Signer ${signerAddress} has zero Circle native USDC on Polygon. Fund ${inputToken} on Polygon PoS.`, - ); - } - - const inputAmount = walletBalance - 20n; - const feeAmount = bpsOf(inputAmount, FEE_BPS); - const bridgeAmount = inputAmount - feeAmount; - - const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; - const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - console.log(`Input USDC: ${ethers.formatUnits(inputAmount, 6)}`); - console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); - console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); - console.log(`TokenMessenger: ${polyCctp.tokenMessenger}`); - console.log(`Burn token: ${polyCctp.usdcAddress}`); - - const depositData = buildDepositForBurnCalldata( - signerAddress, - polyCctp.usdcAddress, - baseCctp.cctpDomain, - bridgeAmount, - true, - ); - - const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const routerIface = new ethers.Interface(ROUTER_ABI); - const execCalldata = buildBridgeCalldata(routerIface, { - signerAddress, - inputToken, - inputAmount, - fee, - tokenMessenger: polyCctp.tokenMessenger, - depositData, - }); - - await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, polyCctp.tokenMessenger); - await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); - - console.log('Sending AllowanceHolder.exec → router.bridge...'); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - inputToken, - inputAmount, - ROUTER_POLYGON, - execCalldata, - ); - - logTxnSummary( - 'Polygon USDC → Base USDC — CCTP — Simple bridge', - CHAIN_IDS.POLYGON, - receipt, - ); - - console.log( - `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`, - ); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/swapBridgeViaOft.ts b/scripts/e2e/swapBridgeViaOft.ts deleted file mode 100644 index 53971f3..0000000 --- a/scripts/e2e/swapBridgeViaOft.ts +++ /dev/null @@ -1,809 +0,0 @@ -/** - * Script — Swap AAVE Polygon → USDT0 Polygon, then bridge to Arbitrum USDT via USDT0 OFT (LayerZero v2) - * - * Two independent scenarios run back-to-back (monolithic + modular each): - * - * Case 1 — AAVE Polygon → USDT0 Polygon (OpenOcean) → Arbitrum USDT (USDT0 OFT bridge) - * 1. OpenOcean swap_quote: AAVE → USDT0 on Polygon (router is sender + recipient of swap) - * 2. Approve AllowanceHolder (0x AH) for the AAVE input amount - * 3. Post-swap fee: FEE_BPS of the OpenOcean USDT0 output amount is transferred to signer EOA - * 4. OFT quote: quoteSend + quoteOFT on the USDT0 OFT Adapter (Polygon) to get LZ nativeFee + amountReceivedLD - * 5. Build send() calldata: amountLD = 0 placeholder, spliced at runtime (byte offset 196) - * 6. Execute via AllowanceHolder.exec(); msg.value = nativeFeeWithBuffer (5% buffer on LZ fee) - * - * Case 2 — USDT0 Polygon → Arbitrum USDT (direct OFT bridge, no swap) - * 1. Pre-bridge fee: FEE_BPS of input USDT0 transferred to signer EOA - * 2. OFT quote + send() calldata (same as above) - * 3. Execute; msg.value = nativeFeeWithBuffer - * - * OFT mechanics (Polygon USDT0 uses OFT_ADAPTER — approval required): - * - Call quoteSend() + quoteOFT() on USDT0_OFT_ADAPTER_POLYGON (dstEid = ARBITRUM_LZ_EID 30110) - * - Approve the OFT Adapter to spend TOKENS.USDT0_POLYGON before calling send() - * - Pass nativeFeeWithBuffer as msg.value (POL on Polygon) so the router forwards LZ fee to the adapter - * - amountLD in send() is spliced at byte offset 196 from the actual post-fee token balance - * - * sendParam.amountLD offset derivation (same as Stargate): - * ABI layout after 4-byte selector: - * sendParam_ptr (32) | fee.nativeFee (32) | fee.lzTokenFee (32) | refundAddress (32) | tail... - * Tail (sendParam body): - * dstEid (32) | to (32) | amountLD (32) ← byte 4 + 3*32 + 2*32 = 196 from calldata start - * - * LZ extraOptions for USDT0 OFT (addExecutorLzReceiveOption(65000, 0)): - * Generated at runtime via @layerzerolabs/lz-v2-utilities Options SDK. - * Equivalent to: type3(0x0003) | workerId(0x01) | optLen(0x0011) | optType(0x01) | uint128(65000) - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaOft.ts all - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaOft.ts aave-usdt0-oft - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaOft.ts usdt0-direct - */ -import axios from 'axios'; -import { ethers } from 'ethers'; -import * as dotenv from 'dotenv'; -import { Options } from '@layerzerolabs/lz-v2-utilities'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - FEE_BPS, - bpsOf, - RPC, - OPEN_OCEAN_API_KEY, - OO_SLIPPAGE_PERCENT, - ALLOWANCE_HOLDER, - ARBITRUM_LZ_EID, - USDT0_OFT_ADAPTER_POLYGON, -} from './config'; -import { - execViaAH, - ensureAllowanceForAllowanceHolder, -} from './utils/allowanceHolder'; -import { - encodeApprove, - encodeTransfer, - encodeBalanceOf, - getWalletErc20Balance, -} from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; -import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, -} from './utils/contractTypes'; -import { sleep } from './utils/sleep'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterApproval, -} from './utils/reproducibility'; - -const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); - -// ─── Constants ──────────────────────────────────────────────────────────────── - -/** Byte offset of sendParam.amountLD within the OFT send() calldata (same as Stargate). */ -const OFT_AMOUNT_LD_OFFSET = 196; - -/** - * LZ executor options for the OFT bridge: TYPE_3 + addExecutorLzReceiveOption(gas=65000, value=0). - * Generated via the @layerzerolabs/lz-v2-utilities SDK (same as oft.service.ts in bungee-backend). - */ -const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); - -// ─── OFT ABI ───────────────────────────────────────────────────────────────── - -/** Minimal OFT / OFT Adapter ABI for quoting and sending. */ -const OFT_ABI = [ - 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', - 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', - 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', -]; - -const OFT_IFACE = new ethers.Interface(OFT_ABI); - -// ─── OpenOcean quote ────────────────────────────────────────────────────────── - -interface OoSwapQuoteResponse { - data: { - to: string; - data: string; - value: string; - outAmount: string; - minOutAmount: string; - }; -} - -/** - * Fetches an OpenOcean swap_quote for AAVE → USDT0 on Polygon. - * The router address is used as both sender and account so tokens land in the router. - */ -async function fetchOpenOceanQuote( - inputAmount: bigint, -): Promise<{ - ooRouter: string; - swapData: string; - estimatedOut: bigint; - minAmountOut: bigint; -}> { - const params: Record = { - inTokenAddress: TOKENS.AAVE_POLYGON, - outTokenAddress: TOKENS.USDT0_POLYGON, - amount: ethers.formatUnits(inputAmount, 18), // AAVE has 18 decimals - slippage: OO_SLIPPAGE_PERCENT, - sender: ROUTER_POLYGON, - account: ROUTER_POLYGON, - gasPrice: '1', - }; - if (OPEN_OCEAN_API_KEY) { - params.apikey = OPEN_OCEAN_API_KEY; - } - - const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; - const response = await axios.get(url, { params }); - const q = response.data.data; - return { - ooRouter: q.to, - swapData: q.data, - estimatedOut: BigInt(q.outAmount), - minAmountOut: BigInt(q.minOutAmount), - }; -} - -// ─── OFT quote ──────────────────────────────────────────────────────────────── - -interface OftQuoteResult { - nativeFee: bigint; - nativeFeeWithBuffer: bigint; - amountReceivedLD: bigint; -} - -/** - * Fetches the LZ nativeFee and expected received amount from the USDT0 OFT Adapter on Polygon. - * - * @param provider JSON-RPC provider for Polygon - * @param bridgeAmountLD Amount of USDT0 (6 decimals on Polygon) to bridge - * @param recipient Recipient address on Arbitrum (also used as refundAddress) - */ -async function fetchOftQuote( - provider: ethers.JsonRpcProvider, - bridgeAmountLD: bigint, - recipient: string, -): Promise { - const contract = new ethers.Contract( - USDT0_OFT_ADAPTER_POLYGON, - OFT_ABI, - provider, - ); - const to32 = ethers.zeroPadValue(recipient, 32); - - const sendParam = { - dstEid: ARBITRUM_LZ_EID, - to: to32, - amountLD: bridgeAmountLD, - minAmountLD: 0n, - extraOptions: LZ_EXTRA_OPTIONS, - composeMsg: '0x', - oftCmd: '0x', - }; - - const [fee, oft] = await Promise.all([ - contract.quoteSend(sendParam, false), - contract.quoteOFT(sendParam), - ]); - - const nativeFee = fee.nativeFee as bigint; - const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; // 5% buffer - - return { - nativeFee, - nativeFeeWithBuffer, - amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, - }; -} - -// ─── OFT send() calldata builder ───────────────────────────────────────────── - -/** - * Encodes the OFT Adapter send() calldata. - * amountLD is set to 0 as a placeholder — the router splices the actual amount - * at byte offset 196 from the router's post-fee token balance at execution time. - * - * @param nativeFee LZ fee in POL (with 5% buffer already applied) - * @param recipient Recipient on Arbitrum (also used as refundAddress) - */ -function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { - return OFT_IFACE.encodeFunctionData('send', [ - { - dstEid: ARBITRUM_LZ_EID, - to: ethers.zeroPadValue(recipient, 32), - amountLD: 0n, // placeholder — spliced at runtime at offset 196 - minAmountLD: 0n, - extraOptions: LZ_EXTRA_OPTIONS, - composeMsg: '0x', - oftCmd: '0x', - }, - { nativeFee, lzTokenFee: 0n }, - recipient, // refundAddress - ]); -} - -// ─── Case 1: AAVE → USDT0 (OpenOcean swap) → USDT0 Base (OFT bridge) ───────── - -/** - * Monolithic for Case 1: - * - Swap AAVE → USDT0 via OpenOcean (swap step) - * - Post-swap fee: FEE_BPS of estimated USDT0 output transferred to signer - * - Bridge remaining USDT0 via OFT Adapter (approval required) - * - bridge amount position flag splices actual balance into amountLD at byte 196 - * - bridge.value = nativeFeeWithBuffer (forwarded as LZ msg.value) - */ -function buildCase1Monolithic( - signer: string, - inputAmount: bigint, - feeAmount: bigint, - minAmountOut: bigint, - ooRouter: string, - swapData: string, - oftSendData: string, - nativeFeeWithBuffer: bigint, -): MonolithicExecutionCall { - return { - exec: { - input: { - user: signer, - inputToken: TOKENS.AAVE_POLYGON, - inputAmount, - }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: TOKENS.USDT0_POLYGON, - value: 0n, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { - receiver: signer, - amount: feeAmount, - }, - bridge: { - target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, // adapter needs ERC-20 approval - value: nativeFeeWithBuffer, // forwarded as LZ native fee - }, - flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), - }, - swapCallData: swapData, - bridgeCallData: oftSendData, - }; -} - -/** - * Modular for Case 1: - * [0] AH.transferFrom(AAVE, signer, router, inputAmount) - * [1] AAVE.approve(ooRouter, inputAmount) - * [2] ooRouter swap calldata — AAVE → USDT0 lands in router - * [3] USDT0.transfer(signer, feeAmount) — post-swap fee to signer - * [4] USDT0.approve(adapter, MaxUint256) — allow adapter to pull USDT0 - * [5] STATICCALL USDT0.balanceOf(router) — capture post-fee balance - * [6] nativeCall adapter.send(...) — spliceWord patches amountLD from [5] - */ -function buildCase1Modular( - signer: string, - inputAmount: bigint, - feeAmount: bigint, - ooRouter: string, - swapData: string, - oftSendData: string, - nativeFeeWithBuffer: bigint, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.AAVE_POLYGON, - signer, - ROUTER_POLYGON, - inputAmount, - ]); - - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, inputAmount)); - exec.call(ooRouter, swapData); // AAVE → USDT0 lands in router - exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signer, feeAmount)); // post-swap fee - exec.call( - TOKENS.USDT0_POLYGON, - encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256), - ); - const usdt0Balance = exec.staticCall( - TOKENS.USDT0_POLYGON, - encodeBalanceOf(ROUTER_POLYGON), - ); - exec - .nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) - .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); - - return exec.toActions(); -} - -async function executeCase1Leg(args: { - label: string; - useModular: boolean; - signer: ethers.Wallet; - signerAddress: string; - provider: ethers.JsonRpcProvider; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { - label, - useModular, - signer, - signerAddress, - provider, - inputAmount, - routerIface, - } = args; - console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - - console.log('Fetching OpenOcean quote (Polygon AAVE → USDT0)...'); - const { ooRouter, swapData, estimatedOut, minAmountOut } = - await fetchOpenOceanQuote(inputAmount); - - const feeAmount = bpsOf(estimatedOut, FEE_BPS); - const bridgeAmount = estimatedOut - feeAmount; - - console.log(` OO router: ${ooRouter}`); - console.log(` Est. USDT0 out: ${ethers.formatUnits(estimatedOut, 6)}`); - console.log( - ` Post-swap fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`, - ); - console.log(` Min USDT0 out: ${ethers.formatUnits(minAmountOut, 6)}`); - console.log(` Bridge amount: ${ethers.formatUnits(bridgeAmount, 6)}`); - - console.log('Fetching USDT0 OFT quote (Polygon → Arbitrum)...'); - const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( - provider, - bridgeAmount, - signerAddress, - ); - - console.log( - ` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} POL`, - ); - console.log( - ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`, - ); - - await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); - - const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - - let execCalldata: string; - if (useModular) { - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - buildCase1Modular( - signerAddress, - inputAmount, - feeAmount, - ooRouter, - swapData, - oftSendData, - nativeFeeWithBuffer, - ), - ]); - } else { - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( - buildCase1Monolithic( - signerAddress, - inputAmount, - feeAmount, - minAmountOut, - ooRouter, - swapData, - oftSendData, - nativeFeeWithBuffer, - ), - )); - } - - await ensureAllowanceForAllowanceHolder( - signer, - TOKENS.AAVE_POLYGON, - inputAmount, - ); - - console.log( - `AllowanceHolder.exec (txValue = ${ethers.formatEther( - nativeFeeWithBuffer, - )} ETH)...`, - ); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - TOKENS.AAVE_POLYGON, - inputAmount, - ROUTER_POLYGON, - execCalldata, - nativeFeeWithBuffer, - ); - - logTxnSummary( - `Arbitrum AAVE → USDT (OO swap) → Arbitrum USDT0 (OFT) — ${ - useModular ? 'Modular' : 'Monolithic' - }`, - CHAIN_IDS.POLYGON, - receipt, - ); -} - -// ─── Case 2: Arbitrum USDT → Base USDT0 (direct OFT bridge, no swap) ───────── - -/** - * Monolithic for Case 2: - * - No swap (NO_SWAP) - * - Pre-bridge fee: FEE_BPS of input USDT0 transferred to signer - * - Bridge remaining USDT0 via OFT Adapter (approval required) - * - bridge amount position flag splices actual balance at byte 196 - * - bridge.value = nativeFeeWithBuffer (forwarded as LZ msg.value) - */ -function buildCase2Monolithic( - signer: string, - inputAmount: bigint, - feeAmount: bigint, - oftSendData: string, - nativeFeeWithBuffer: bigint, -): MonolithicExecutionCall { - return { - exec: { - input: { - user: signer, - inputToken: TOKENS.USDT0_POLYGON, - inputAmount, - }, - preFee: { - receiver: signer, - amount: feeAmount, - }, - swap: NO_SWAP, - postFee: NO_FEE, - bridge: { - target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, - value: nativeFeeWithBuffer, - }, - flags: bridgeAmountPositionFlag(OFT_AMOUNT_LD_OFFSET), - }, - swapCallData: '0x', - bridgeCallData: oftSendData, - }; -} - -/** - * Modular for Case 2: - * [0] AH.transferFrom(USDT0, signer, router, inputAmount) - * [1] USDT0.transfer(signer, feeAmount) — pre-bridge fee to signer - * [2] USDT0.approve(adapter, MaxUint256) — allow adapter to pull USDT0 - * [3] STATICCALL USDT0.balanceOf(router) — capture post-fee balance - * [4] nativeCall adapter.send(...) — spliceWord patches amountLD from [3] - */ -function buildCase2Modular( - signer: string, - inputAmount: bigint, - feeAmount: bigint, - oftSendData: string, - nativeFeeWithBuffer: bigint, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const ahTransferFromData = ahIface.encodeFunctionData('transferFrom', [ - TOKENS.USDT0_POLYGON, - signer, - ROUTER_POLYGON, - inputAmount, - ]); - - const exec = new ModularActionsBuilder(); - exec.call(ALLOWANCE_HOLDER, ahTransferFromData); - exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signer, feeAmount)); // pre-bridge fee - exec.call( - TOKENS.USDT0_POLYGON, - encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256), - ); - const usdt0Balance = exec.staticCall( - TOKENS.USDT0_POLYGON, - encodeBalanceOf(ROUTER_POLYGON), - ); - exec - .nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) - .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); - - return exec.toActions(); -} - -async function executeCase2Leg(args: { - label: string; - useModular: boolean; - signer: ethers.Wallet; - signerAddress: string; - provider: ethers.JsonRpcProvider; - inputAmount: bigint; - routerIface: ethers.Interface; -}): Promise { - const { - label, - useModular, - signer, - signerAddress, - provider, - inputAmount, - routerIface, - } = args; - console.log(`\n── ${label} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - - const feeAmount = bpsOf(inputAmount, FEE_BPS); - const bridgeAmount = inputAmount - feeAmount; - - console.log(` Input USDT0: ${ethers.formatUnits(inputAmount, 6)}`); - console.log( - ` Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`, - ); - console.log(` Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); - - console.log('Fetching USDT0 OFT quote (Polygon → Arbitrum)...'); - const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( - provider, - bridgeAmount, - signerAddress, - ); - - console.log( - ` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} POL`, - ); - console.log( - ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`, - ); - - await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); - - const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); - - let execCalldata: string; - if (useModular) { - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - buildCase2Modular( - signerAddress, - inputAmount, - feeAmount, - oftSendData, - nativeFeeWithBuffer, - ), - ]); - } else { - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs( - buildCase2Monolithic( - signerAddress, - inputAmount, - feeAmount, - oftSendData, - nativeFeeWithBuffer, - ), - )); - } - - await ensureAllowanceForAllowanceHolder( - signer, - TOKENS.USDT0_POLYGON, - inputAmount, - ); - - console.log( - `AllowanceHolder.exec (txValue = ${ethers.formatEther( - nativeFeeWithBuffer, - )} ETH)...`, - ); - const receipt = await execViaAH( - signer, - ROUTER_POLYGON, - TOKENS.USDT0_POLYGON, - inputAmount, - ROUTER_POLYGON, - execCalldata, - nativeFeeWithBuffer, - ); - - logTxnSummary( - `Polygon USDT → Arbitrum USDT0 (OFT direct) — ${ - useModular ? 'Modular' : 'Monolithic' - }`, - CHAIN_IDS.POLYGON, - receipt, - ); -} - -// ─── Case runners ───────────────────────────────────────────────────────────── - -async function runCase1( - signer: ethers.Wallet, - signerAddress: string, - routerIface: ethers.Interface, -): Promise { - console.log(`\n${'═'.repeat(70)}`); - console.log( - 'CASE 1: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (OFT bridge)', - ); - console.log('═'.repeat(70)); - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signerOnChain = signer.connect(provider); - - const { balance: walletBalance } = await getWalletErc20Balance( - TOKENS.AAVE_POLYGON, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Case 1: signer ${signerAddress} has zero AAVE on Polygon. Fund ${TOKENS.AAVE_POLYGON}.`, - ); - } - - const legAmount = (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error('Case 1: AAVE balance too small to split into two halves.'); - } - - console.log( - `Input token (AAVE): ${ethers.formatUnits( - walletBalance, - 18, - )} (full balance)`, - ); - console.log(`Per leg: ${ethers.formatUnits(legAmount, 18)}`); - - await executeCase1Leg({ - label: '1/2', - useModular: false, - signer: signerOnChain, - signerAddress, - provider, - inputAmount: legAmount, - routerIface, - }); - - console.log('\nSleeping 3s before modular leg...'); - await sleep(3000); - - await executeCase1Leg({ - label: '2/2', - useModular: true, - signer: signerOnChain, - signerAddress, - provider, - inputAmount: legAmount, - routerIface, - }); -} - -async function runCase2( - signer: ethers.Wallet, - signerAddress: string, - routerIface: ethers.Interface, -): Promise { - console.log(`\n${'═'.repeat(70)}`); - console.log( - 'CASE 2: Polygon USDT0 → Arbitrum USDT0 (direct OFT bridge, no swap)', - ); - console.log('═'.repeat(70)); - - const provider = new ethers.JsonRpcProvider(RPC.POLYGON); - const signerOnChain = signer.connect(provider); - - const { balance: walletBalance } = await getWalletErc20Balance( - TOKENS.USDT0_POLYGON, - signerAddress, - provider, - ); - if (walletBalance === 0n) { - throw new Error( - `Case 2: signer ${signerAddress} has zero USDT0 on Polygon. Fund ${TOKENS.USDT0_POLYGON}.`, - ); - } - - const legAmount = (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error( - 'Case 2: USDT0 balance too small to split into two halves.', - ); - } - - console.log( - `Input token (USDT0): ${ethers.formatUnits( - walletBalance, - 6, - )} (full balance)`, - ); - console.log(`Per leg: ${ethers.formatUnits(legAmount, 6)}`); - - await executeCase2Leg({ - label: '1/2', - useModular: false, - signer: signerOnChain, - signerAddress, - provider, - inputAmount: legAmount, - routerIface, - }); - - console.log('\nSleeping 3s before modular leg...'); - await sleep(3000); - - await executeCase2Leg({ - label: '2/2', - useModular: true, - signer: signerOnChain, - signerAddress, - provider, - inputAmount: legAmount, - routerIface, - }); -} - -// ─── Main ───────────────────────────────────────────────────────────────────── - -async function main() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const signer = new ethers.Wallet(privateKey); - const signerAddress = await signer.getAddress(); - const routerIface = new ethers.Interface(ROUTER_ABI); - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${ROUTER_POLYGON}`); - - const caseArg = process.argv[2]?.toLowerCase(); - - if (caseArg === 'usdt0-direct') { - await runCase2(signer, signerAddress, routerIface); - console.log( - '\nCase 2 complete — USDT0 arrives on Arbitrum once LZ delivers the message.', - ); - return; - } - if (caseArg === 'aave-usdt0-oft') { - await runCase1(signer, signerAddress, routerIface); - console.log( - '\nCase 1 complete — USDT0 arrives on Arbitrum once LZ delivers the message.', - ); - return; - } - - console.error( - `Unknown case: ${caseArg}. Use: all | aave-usdt0-oft | usdt0-direct`, - ); - process.exit(1); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/swapBridgeViaStargateNative.ts b/scripts/e2e/swapBridgeViaStargateNative.ts deleted file mode 100644 index 2aa3aea..0000000 --- a/scripts/e2e/swapBridgeViaStargateNative.ts +++ /dev/null @@ -1,1100 +0,0 @@ -/** - * Stargate e2e test script — three independent cases, each running a - * monolithic leg followed (after a 3-second pause) by a modular leg. - * - * Case 1 Arbitrum USDC → OO swap → native ETH → Stargate Native ETH Pool → Base ETH - * Case 2 Polygon USDC → (no swap) → Stargate USDC Pool → Base USDC - * Case 3 Base USDC → OO swap → native ETH → Stargate Native ETH Pool → Arb ETH - * Case 4 Polygon POL → OO swap → Polygon USDT0 → USDT0 OFT Adapter → Arbitrum USDT0 - * - * Native-pool mechanics (cases 1 & 3): - * send() requires msg.value >= amountLD + nativeFee (StargatePoolNative._assertMessagingFee). - * Monolithic: BRIDGE_VALUE_FLAG + BRIDGE_AMOUNT_POSITION_FLAG set. - * Router splices finalETH into amountLD at runtime; msg.value = finalETH + nativeFeeWithBuffer. - * finalETH + nativeFeeWithBuffer >= finalETH + nativeFee ✓; destination gets exact finalETH. - * Modular: amountLD = minAmountOut - fee - nativeFeeWithBuffer (static; no splice available). - * nativeCall Stargate with value = amountLD + nativeFeeWithBuffer = minAmountOut - fee. - * - * ERC20-pool mechanics (case 2): - * send() uses ERC20 transferFrom for USDC; msg.value = nativeFee only. - * Monolithic: bridge amount position flag set to 196, bridge.value=nativeFeeWithBuffer. - * Modular: staticCall USDC.balanceOf(router) → spliceWord(196n) into Stargate calldata. - * nativeCall Stargate with value = nativeFeeWithBuffer. - * - * Case selection (required) — same idea as `bridgeViaRelay.ts` / `swapBridgeViaCctp.ts`: - * Pass a scenario as the first CLI arg, or set `STARGATE_E2E_CASE` when your runner - * cannot pass argv. - * - * 1 / arb-usdc-base-eth Arbitrum USDC → OO → native ETH → Stargate native → Base ETH - * 2 / polygon-usdc-base Polygon USDC → Stargate USDC pool → Base USDC (no swap) - * 3 / base-usdc-arb-eth Base USDC → OO → native ETH → Stargate native → Arbitrum ETH - * 4 / polygon-pol-usdt0-arb Polygon POL → OO → Polygon USDT0 → LZ OFT Adapter → Arb USDT0 - * msg.value = inputPOL used in OO swap + LZ nativeFee (POL) - * - * Usage: - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb direct - * PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb allowance-holder - * STARGATE_E2E_CASE=4 STARGATE_ROUTER_EXEC=direct PRIVATE_KEY=0x... ts-node scripts/e2e/swapBridgeViaStargateNative.ts - * - * Router execution (`argv[3]` overrides `STARGATE_ROUTER_EXEC`): - * - * | Mode | Behaviour | - * |---------------------|-----------| - * | `direct` | Signer sends tx directly to router with `{ value }` | - * | `allowance-holder` | `AllowanceHolder.exec` wraps router (`msg.value` + ERC-2771 user suffix) | - * - * **Native-token input (case 4):** choose explicitly — either pass `direct` or `allowance-holder` as argv[3], - * or set `STARGATE_ROUTER_EXEC`. There is no default; ambiguous runs exit with usage. - * - * **ERC20 input (cases 1–3):** defaults to `allowance-holder`; `direct` is rejected (AH pull required). - * - * Router per source chain: `ROUTER_BY_CHAIN_ID` / `routerAddressForChain(chainId)` in config.ts (`ROUTER_CHAIN_` overrides). - * - * Bridge note: Case 4 uses LayerZero **`send`** on {@link USDT0_OFT_ADAPTER_POLYGON}, not Stargate. - * ABI matches Stargate pool `send`; `lzExtraOptions` uses TYPE_3 executor gas (same as `swapBridgeViaOft.ts`). - */ -import axios from 'axios'; -import { ethers, parseEther } from 'ethers'; -import * as dotenv from 'dotenv'; -import { Options } from '@layerzerolabs/lz-v2-utilities'; -dotenv.config(); - -import { - CHAIN_IDS, - routerAddressForChain, - TOKENS, - FEE_BPS, - bpsOf, - RPC, - OPEN_OCEAN_API_KEY, - OO_SLIPPAGE_PERCENT, - ALLOWANCE_HOLDER, - NATIVE_TOKEN_ADDRESS, - STARGATE_NATIVE_ARB, - STARGATE_NATIVE_BASE, - STARGATE_USDC_POLYGON, - USDT0_OFT_ADAPTER_POLYGON, - BASE_LZ_EID, - ARBITRUM_LZ_EID, - STARGATE_AMOUNT_LD_OFFSET, -} from './config'; -import { execViaAH, execDirect, ensureAllowanceForAllowanceHolder } from './utils/allowanceHolder'; -import { - encodeApprove, - encodeTransfer, - encodeBalanceOf, - getWalletErc20Balance, -} from './utils/erc20'; -import { ROUTER_ABI } from './utils/routerAbi'; -import { ModularActionsBuilder } from './utils/modularActionsBuilder/index'; -import type { ModularAction } from './utils/modularActionsBuilder/index'; -import { - BRIDGE_VALUE_FLAG, - MonolithicExecutionCall, - NO_FEE, - NO_SWAP, - ZERO_ADDRESS, - ZERO_BYTES32, - bridgeAmountPositionFlag, - monolithicArgs, -} from './utils/contractTypes'; -import { sleep } from './utils/sleep'; -import { logTxnSummary } from './utils/txnLogSummary'; -import { - ensureRouterErc20Balance, - ensureRouterNativeBalance, - ensureRouterApproval, -} from './utils/reproducibility'; - -/** - * LZ extra options for Polygon USDT0 OFT Adapter `send()` (executor gas). - * Mirrors `swapBridgeViaOft.ts`. - */ -const LZ_EXTRA_OPTIONS_POLYGON_USDT0 = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); - -// ─── Case configuration ─────────────────────────────────────────────────────── - -/** - * Describes a Stargate test case. `ooSwap` being null means case 2 (no swap — input - * token goes directly to Stargate). `isNativePool` drives the bridge mechanics. - */ -interface OoSwapConfig { - inToken: string; - outToken: string; - inDecimals: number; - chainId: number; - gasPrice: string; -} - -interface CaseConfig { - name: string; - sourceChainId: number; - rpc: string; - inputToken: string; - inputDecimals: number; - /** true when inputToken is native (POL/ETH/BNB); exec mode must be set explicitly (`direct` | `allowance-holder`) */ - isNativeInput: boolean; - ooSwap: OoSwapConfig | null; // null → skip OO swap, bridge input token directly - /** Contract that receives LZ `send` calldata — Stargate pool or LZ OFT adapter (same ABI shape). */ - bridgeContract: string; - /** `extraOptions` in SendParam (`0x` for Stargate pools; encoded TYPE_3 for USDT0 OFT on Polygon). */ - lzExtraOptions: string; - isNativePool: boolean; - destLzEid: number; -} - -const CASES: CaseConfig[] = [ - { - name: 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate Native Pool)', - sourceChainId: CHAIN_IDS.ARBITRUM, - rpc: RPC.ARBITRUM, - inputToken: TOKENS.USDC_ARB, - inputDecimals: 6, - isNativeInput: false, - ooSwap: { - inToken: TOKENS.USDC_ARB, - outToken: NATIVE_TOKEN_ADDRESS, - inDecimals: 6, - chainId: CHAIN_IDS.ARBITRUM, - gasPrice: '1', - }, - bridgeContract: STARGATE_NATIVE_ARB, - lzExtraOptions: '0x', - isNativePool: true, - destLzEid: BASE_LZ_EID, - }, - { - name: 'Polygon USDC → Base USDC (Stargate USDC Pool, no swap)', - sourceChainId: CHAIN_IDS.POLYGON, - rpc: RPC.POLYGON, - inputToken: TOKENS.USDC_POLYGON_CIRCLE, - inputDecimals: 6, - isNativeInput: false, - ooSwap: null, // skip OO swap — bridge USDC directly - bridgeContract: STARGATE_USDC_POLYGON, - lzExtraOptions: '0x', - isNativePool: false, - destLzEid: BASE_LZ_EID, - }, - { - name: 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate Native Pool)', - sourceChainId: CHAIN_IDS.BASE, - rpc: RPC.BASE, - inputToken: TOKENS.USDC_BASE, - inputDecimals: 6, - isNativeInput: false, - ooSwap: { - inToken: TOKENS.USDC_BASE, - outToken: NATIVE_TOKEN_ADDRESS, - inDecimals: 6, - chainId: CHAIN_IDS.BASE, - gasPrice: '1', - }, - bridgeContract: STARGATE_NATIVE_BASE, - lzExtraOptions: '0x', - isNativePool: true, - destLzEid: ARBITRUM_LZ_EID, - }, - { - // Polygon native POL → USDT0 via OpenOcean → Arbitrum USDT0 via USDT0 OFT Adapter on Polygon (LayerZero `send`). - name: 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (LZ OFT Adapter)', - sourceChainId: CHAIN_IDS.POLYGON, - rpc: RPC.POLYGON, - inputToken: NATIVE_TOKEN_ADDRESS, - inputDecimals: 18, - isNativeInput: true, - ooSwap: { - inToken: NATIVE_TOKEN_ADDRESS, - outToken: TOKENS.USDT0_POLYGON, - inDecimals: 18, - chainId: CHAIN_IDS.POLYGON, - gasPrice: '1', - }, - bridgeContract: USDT0_OFT_ADAPTER_POLYGON, - lzExtraOptions: LZ_EXTRA_OPTIONS_POLYGON_USDT0, - isNativePool: false, - destLzEid: ARBITRUM_LZ_EID, - }, -]; - -/** Slug aliases (and `1`/`2`/`3`/`4`) → index in `CASES`. */ -const STARGATE_SCENARIO_ALIASES: Record = { - '1': 0, - 'arb-usdc-base-eth': 0, - 'arb-native-base': 0, - 'arbitrum-usdc-base-eth': 0, - - '2': 1, - 'polygon-usdc-base': 1, - 'usdc-polygon-base': 1, - - '3': 2, - 'base-usdc-arb-eth': 2, - 'base-native-arb': 2, - - '4': 3, - 'polygon-pol-usdt0-arb': 3, - 'polygon-pol-arb-usdt0': 3, - 'pol-native-usdt0-arb': 3, -}; - -/** - * Resolves scenario from CLI (`process.argv[2]`) or `STARGATE_E2E_CASE`, then - * returns the matching `CaseConfig`. Fails fast with a usage message if unset/unknown. - */ -function resolveScenarioConfig(): CaseConfig { - const raw = (process.argv[2] ?? process.env.STARGATE_E2E_CASE ?? '').trim().toLowerCase(); - if (!raw) { - console.error( - 'Missing scenario. Pass argv[2] or set STARGATE_E2E_CASE. Examples:\n' + - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts arb-usdc-base-eth\n' + - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-usdc-base\n' + - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts base-usdc-arb-eth\n' + - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb\n' + - 'Or use numeric slugs 1 | 2 | 3 | 4.', - ); - process.exit(1); - } - const idx = STARGATE_SCENARIO_ALIASES[raw]; - if (idx === undefined || !CASES[idx]) { - console.error(`Unknown Stargate e2e scenario "${raw}". Valid: ${Object.keys(STARGATE_SCENARIO_ALIASES).sort().join(', ')}`); - process.exit(1); - } - return CASES[idx]; -} - -/** How the signer reaches the router: direct `eth_sendTransaction`, or wrapped `AllowanceHolder.exec`. */ -type RouterExecRoute = 'direct' | 'allowance-holder'; - -/** argv[3] / `STARGATE_ROUTER_EXEC` tokens → canonical route. */ -const ROUTER_EXEC_ALIASES: Record = { - direct: 'direct', - dr: 'direct', - router: 'direct', - - 'allowance-holder': 'allowance-holder', - ah: 'allowance-holder', - exec: 'allowance-holder', -}; - -/** - * Resolves execution transport: **`argv[3]` overrides `STARGATE_ROUTER_EXEC`** when non-empty after trim. - * - * - **Native-token input (`isNativeInput`):** caller **must** set `direct` or `allowance-holder` explicitly - * — no silent default — so AH vs signer→router stays a deliberate choice. - * - **ERC20 input:** defaults to `allowance-holder`; `direct` is rejected (`AllowanceHolder.transferFrom` pull). - */ -function resolveRouterExecRoute(cfg: CaseConfig): RouterExecRoute { - const rawArg = typeof process.argv[3] === 'string' ? process.argv[3].trim().toLowerCase() : ''; - const rawEnv = (process.env.STARGATE_ROUTER_EXEC ?? '').trim().toLowerCase(); - const raw = rawArg || rawEnv; - - const resolveExplicit = (): RouterExecRoute | null => { - if (!raw) { - return null; - } - const route = ROUTER_EXEC_ALIASES[raw]; - if (!route) { - console.error( - `Unknown router exec "${raw}". Use argv[3] or STARGATE_ROUTER_EXEC: direct | allowance-holder (aliases dr, router, ah, exec).`, - ); - process.exit(1); - } - return route; - }; - - const route = resolveExplicit(); - if (route !== null) { - if (!cfg.isNativeInput && route === 'direct') { - console.error( - 'ERC20 input cases cannot use direct router txs: `_pullFromUser` invokes AllowanceHolder.transferFrom, which needs the ephemeral allowance set by AH.exec.', - ); - process.exit(1); - } - return route; - } - - if (cfg.isNativeInput) { - console.error( - [ - 'Native-token input scenarios require an explicit router exec mode (no default).', - '', - ' argv[3] STARGATE_ROUTER_EXEC', - ' ----------------------------- ------------------------------', - ' direct direct', - ' allowance-holder (aliases: ah, exec)', - '', - 'Examples:', - ' ts-node scripts/e2e/swapBridgeViaStargateNative.ts polygon-pol-usdt0-arb direct', - ' STARGATE_ROUTER_EXEC=allowance-holder ts-node scripts/e2e/swapBridgeViaStargateNative.ts 4', - ].join('\n'), - ); - process.exit(1); - } - - return 'allowance-holder'; -} - -// ─── Shared Stargate ABI ────────────────────────────────────────────────────── - -/** Minimal Stargate pool ABI fragments — identical for native and ERC20 pools. */ -const STARGATE_ABI = [ - 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', - 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', - 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', -]; - -const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); - -// ─── OpenOcean quote ────────────────────────────────────────────────────────── - -interface OoQuoteResponse { - data: { - to: string; - data: string; - /** wei to forward with OO call for native-token sells (omit or "0" for ERC20 sells) */ - value?: string; - outAmount: string; - minOutAmount: string; - }; -} - -/** - * Fetches an OpenOcean swap_quote. - * `amount` is in the input token's native units (raw bigint). - */ -async function fetchOoQuote( - cfg: OoSwapConfig, - routerAddress: string, - amount: bigint, -): Promise<{ - ooRouter: string; - swapData: string; - estimatedOut: bigint; - minAmountOut: bigint; - /** OO-recommended wei for swap calldata (`value` field); prefer over raw `amount` when > 0 */ - nativeSwapWei: bigint; -}> { - const params: Record = { - inTokenAddress: cfg.inToken, - outTokenAddress: cfg.outToken, - amount: ethers.formatUnits(amount, cfg.inDecimals), - slippage: OO_SLIPPAGE_PERCENT, - sender: routerAddress, - account: routerAddress, - gasPrice: cfg.gasPrice, - }; - if (OPEN_OCEAN_API_KEY) { - params.apikey = OPEN_OCEAN_API_KEY; - } - const url = `https://open-api.openocean.finance/v3/${cfg.chainId}/swap_quote`; - const response = await axios.get(url, { params }); - const q = response.data.data; - const nativeSwapWeiRaw = q.value !== undefined && q.value !== '' ? BigInt(q.value) : 0n; - return { - ooRouter: q.to, - swapData: q.data, - estimatedOut: BigInt(q.outAmount), - minAmountOut: BigInt(q.minOutAmount), - nativeSwapWei: nativeSwapWeiRaw, - }; -} - -// ─── Stargate quote ─────────────────────────────────────────────────────────── - -/** - * Fetches the LZ nativeFee and expected receive amount from Stargate. - * - * @param pool Pool contract address on the source chain - * @param provider Provider for the source chain - * @param destLzEid LayerZero destination EID - * @param recipient Recipient on destination (also refundAddress) - * @param bridgeAmountLD Tentative bridge amount for the quote - * @param extraOptions `SendParam.extraOptions` — `'0x'` for Stargate pools; LZ TYPE_3 for USDT0 OFT adapter - */ -async function fetchStargateQuote( - pool: string, - provider: ethers.JsonRpcProvider, - destLzEid: number, - recipient: string, - bridgeAmountLD: bigint, - extraOptions: string, -): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { - const contract = new ethers.Contract(pool, STARGATE_ABI, provider); - const to32 = ethers.zeroPadValue(recipient, 32); - const sendParam = { - dstEid: destLzEid, - to: to32, - amountLD: bridgeAmountLD, - minAmountLD: 0n, - extraOptions, - composeMsg: '0x', - oftCmd: '0x', - }; - const [fee, oft] = await Promise.all([ - contract.quoteSend(sendParam, false), - contract.quoteOFT(sendParam), - ]); - return { - nativeFee: fee.nativeFee as bigint, - amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, - }; -} - -// ─── Stargate calldata builder ──────────────────────────────────────────────── - -/** - * Encodes Stargate send() calldata. - * - * For native pools: pass a pre-computed amountLD; no splice required. - * For ERC20 pools: pass amountLD=0 as a placeholder; caller splices the - * real amount at STARGATE_AMOUNT_LD_OFFSET (196 bytes). - * - * @param destLzEid Destination LZ endpoint ID - * @param nativeFee LZ fee in source-chain native token (with buffer) - * @param recipient Recipient address on destination chain - * @param amountLD Explicit amountLD (for native pools); 0n for ERC20 pools - * @param extraOptions `SendParam.extraOptions` - */ -function buildStargateCalldata( - destLzEid: number, - nativeFee: bigint, - recipient: string, - amountLD: bigint, - extraOptions: string, -): string { - return STARGATE_IFACE.encodeFunctionData('send', [ - { - dstEid: destLzEid, - to: ethers.zeroPadValue(recipient, 32), - amountLD, - minAmountLD: 0n, - extraOptions, - composeMsg: '0x', - oftCmd: '0x', - }, - { nativeFee, lzTokenFee: 0n }, - recipient, // refundAddress - ]); -} - -// ─── Monolithic builders ────────────────────────────────────────────────────── - -/** - * Monolithic for native-pool cases (cases 1 & 3): - * - OO swap input token → native ETH - * - BRIDGE_VALUE_FLAG + BRIDGE_AMOUNT_POSITION_FLAG: router splices finalETH into amountLD at - * runtime and forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate - * - StargatePoolNative checks msg.value >= amountLD + nativeFee; satisfied since - * finalETH + nativeFeeWithBuffer >= finalETH + nativeFee ✓ - */ -function buildNativePoolMonolithic( - signer: string, - cfg: CaseConfig, - inputAmount: bigint, - feeAmount: bigint, - minAmountOut: bigint, - ooRouter: string, - swapData: string, - stargateData: string, - nativeFeeWithBuffer: bigint, -): MonolithicExecutionCall { - return { - exec: { - input: { user: signer, inputToken: cfg.inputToken, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ooRouter, - outputToken: NATIVE_TOKEN_ADDRESS, - value: 0n, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signer, amount: feeAmount }, - bridge: { - target: cfg.bridgeContract, - approvalSpender: ZERO_ADDRESS, // no ERC20 approval for native ETH - value: nativeFeeWithBuffer, // added to finalETH as msg.value by BRIDGE_VALUE_FLAG - }, - flags: BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: swapData, - bridgeCallData: stargateData, - }; -} - -/** - * Monolithic for ERC20-pool case (case 2): - * - No OO swap (NO_SWAP) — input USDC goes directly to bridge - * - USDC transferred via ERC20 approval - * - bridge amount position flag set to 196: router splices finalAmount into amountLD at runtime - * - bridge.value=nativeFeeWithBuffer: forwarded as msg.value for the LZ fee - */ -function buildErc20PoolMonolithic( - signer: string, - cfg: CaseConfig, - inputAmount: bigint, - feeAmount: bigint, - stargateData: string, - nativeFeeWithBuffer: bigint, -): MonolithicExecutionCall { - return { - exec: { - input: { user: signer, inputToken: cfg.inputToken, inputAmount }, - preFee: NO_FEE, - swap: NO_SWAP, // skip swap — finalToken = inputToken, finalAmount = inputAmount - preFee - postFee: { receiver: signer, amount: feeAmount }, - bridge: { - target: cfg.bridgeContract, - approvalSpender: cfg.bridgeContract, // router must approve USDC to pool - value: nativeFeeWithBuffer, // POL/native forwarded as LZ fee msg.value - }, - flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: '0x', - bridgeCallData: stargateData, - }; -} - -// ─── Modular builders ───────────────────────────────────────────────────────── - -/** - * Modular for native-pool cases (cases 1 & 3): - * [0] AH.transferFrom input token - * [1] approve(ooRouter, inputAmount) - * [2] OO swap → native ETH lands in router - * [3] nativeCall: send fee ETH to signer - * [4] nativeCall: Stargate send() with value = amountLD + nativeFeeWithBuffer = minAmountOut - fee - * - * amountLD (from stargateData) = minAmountOut - fee - nativeFeeWithBuffer. - * StargatePoolNative check: msg.value >= amountLD + nativeFee; - * bridgeValue = amountLD + nativeFeeWithBuffer >= amountLD + nativeFee ✓ - * Any ETH surplus over minAmountOut stays in the router as unspent value. - */ -function buildNativePoolModularActions( - signer: string, - routerAddress: string, - cfg: CaseConfig, - inputAmount: bigint, - feeAmount: bigint, - nativeFeeWithBuffer: bigint, - minAmountOut: bigint, - ooRouter: string, - swapData: string, - stargateData: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const exec = new ModularActionsBuilder(); - - exec.call( - ALLOWANCE_HOLDER, - ahIface.encodeFunctionData('transferFrom', [cfg.inputToken, signer, routerAddress, inputAmount]), - ); - exec.call(cfg.inputToken, encodeApprove(ooRouter, inputAmount)); - exec.call(ooRouter, swapData); // USDC → native ETH lands in router - exec.nativeCall(signer, '0x', feeAmount); // post-swap fee in ETH - // Bridge: value = amountLD + nativeFeeWithBuffer = minAmountOut - feeAmount - const bridgeValue = minAmountOut - feeAmount; - exec.nativeCall(cfg.bridgeContract, stargateData, bridgeValue); - - return exec.toActions(); -} - -/** - * Modular for ERC20-pool case (case 2): - * [0] AH.transferFrom USDC - * [1] USDC.transfer(signer, fee) - * [2] USDC.approve(stargatePool, MaxUint256) - * [3] STATICCALL USDC.balanceOf(router) — return value spliced into [4] - * [4] nativeCall: Stargate send() with nativeFeeWithBuffer POL; - * splicePayloadWord(STARGATE_AMOUNT_LD_OFFSET): CALL_WITH_NATIVE data is - * [32-byte native value prefix][ethers send calldata]; amountLD stays at +196 - * within the payload slice (matches OpenOceanStargateNativeOpenRouterPoC.t.sol). - */ -function buildErc20PoolModularActions( - signer: string, - routerAddress: string, - cfg: CaseConfig, - inputAmount: bigint, - feeAmount: bigint, - nativeFeeWithBuffer: bigint, - stargateData: string, -): ModularAction[] { - const ahIface = new ethers.Interface([ - 'function transferFrom(address token, address owner, address recipient, uint256 amount)', - ]); - const exec = new ModularActionsBuilder(); - - exec.call( - ALLOWANCE_HOLDER, - ahIface.encodeFunctionData('transferFrom', [cfg.inputToken, signer, routerAddress, inputAmount]), - ); - exec.call(cfg.inputToken, encodeTransfer(signer, feeAmount)); // USDC fee to signer - exec.call(cfg.inputToken, encodeApprove(cfg.bridgeContract, ethers.MaxUint256)); - const usdcBalance = exec.staticCall(cfg.inputToken, encodeBalanceOf(routerAddress)); - exec - .nativeCall(cfg.bridgeContract, stargateData, nativeFeeWithBuffer) - .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); - - return exec.toActions(); -} - -/** - * Fallback gas reserve used in `runCase` when splitting the balance into leg amounts. - * The actual safety budget used in `executeLeg` is derived dynamically from the - * provider's current `maxFeePerGas` (see `NATIVE_INPUT_GAS_LIMIT_ESTIMATE`). - */ -const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); - -/** - * Estimated gas units for a native-input leg (generous upper bound covering the - * modular path with OO multi-hop swap + LZ OFT send on Polygon). - * Actual usage is ~1M–1.1M; using 2M × maxFeePerGas gives a comfortable ceiling. - */ -const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; - -// ─── Monolithic/modular builders for case 4 ─────────────────────────────────── - -/** - * Monolithic for case 4 (native gas token input → OO swap to bridged ERC-20 → LZ `send` on adapter/pool): - * - inputToken = NATIVE_TOKEN_ADDRESS; swap.approvalSpender = 0 (no ERC20 approve needed) - * - swap.value = `ooSwapNativeWei` (OpenOcean `value` field when present; else quoted input wei) - * - postFee: router sends feeAmount of OO output token (e.g. USDT0) to signer - * - bridge: ERC-20 pool mechanics — splice post-fee balance into amountLD at offset 196 - * - * msg.value ≈ ooSwapNativeWei + nativeFeeWithBuffer (signer attaches full `txValue`; OO consumes POL/ETH swap leg). - */ -function buildNativeInErc20BridgeMonolithic( - signer: string, - cfg: CaseConfig, - inputAmount: bigint, - feeAmount: bigint, - minAmountOut: bigint, - ooRouter: string, - swapData: string, - stargateData: string, - nativeFeeWithBuffer: bigint, - ooSwapNativeWei: bigint, -): MonolithicExecutionCall { - const rawOoWei = ooSwapNativeWei > 0n ? ooSwapNativeWei : inputAmount; - const polOrEthToOo = rawOoWei <= inputAmount ? rawOoWei : inputAmount; - return { - exec: { - input: { user: signer, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }, - preFee: NO_FEE, - swap: { - target: ooRouter, - approvalSpender: ZERO_ADDRESS, // no ERC20 approve for native ETH input - outputToken: cfg.ooSwap!.outToken, - value: polOrEthToOo, - minOutput: minAmountOut, - returnDataWordOffset: 0n, - }, - postFee: { receiver: signer, amount: feeAmount }, // fee in OO output token (USDC/USDT0) - bridge: { - target: cfg.bridgeContract, - approvalSpender: cfg.bridgeContract, // router approves bridge contract to pull ERC20 - value: nativeFeeWithBuffer, // LZ fee in native gas token only - }, - flags: bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET), - }, - swapCallData: swapData, - bridgeCallData: stargateData, - }; -} - -/** - * Modular for case 4 (native gas token in → OO → ERC-20 out → LZ `send`): - * [0] nativeCall(ooRouter, swapData, ooSwapWei) — OO `value` when present else leg `inputAmount` - * … same ERC-20 fee / approve / splice as monolithic ERC20-pool bridge path. - * - * Input native is forwarded on the enclosing tx (`txValue`); no AH.transferFrom pull. - */ -function buildNativeInErc20BridgeModularActions( - signer: string, - routerAddress: string, - cfg: CaseConfig, - inputAmount: bigint, - feeAmount: bigint, - nativeFeeWithBuffer: bigint, - ooRouter: string, - swapData: string, - stargateData: string, - ooSwapNativeWei: bigint, -): ModularAction[] { - const exec = new ModularActionsBuilder(); - const outTokenAddr = cfg.ooSwap!.outToken; - const rawOoWei = ooSwapNativeWei > 0n ? ooSwapNativeWei : inputAmount; - const polOrEthToOo = rawOoWei <= inputAmount ? rawOoWei : inputAmount; - - exec.nativeCall(ooRouter, swapData, polOrEthToOo); - exec.call(outTokenAddr, encodeTransfer(signer, feeAmount)); - exec.call(outTokenAddr, encodeApprove(cfg.bridgeContract, ethers.MaxUint256)); - const tokenBal = exec.staticCall(outTokenAddr, encodeBalanceOf(routerAddress)); - exec - .nativeCall(cfg.bridgeContract, stargateData, nativeFeeWithBuffer) - .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), tokenBal.returnWord()); - - return exec.toActions(); -} - -// ─── Execution leg ──────────────────────────────────────────────────────────── - -/** - * Dispatches tx to router either as a signer→router `{ value }` call or wrapped in - * `AllowanceHolder.exec` (ERC-2771 suffix so `_msgSender()` resolves inside router). - * - * ERC20 `_pullFromUser` requires ephemeral AH allowance ⇒ `allowance-holder` only for non-native inputs. - */ -async function dispatchRouterTransaction( - route: RouterExecRoute, - cfg: CaseConfig, - signer: ethers.Signer, - routerAddress: string, - execCalldata: string, - inputAmount: bigint, - txValue: bigint, - nativeSymbol: string, -): Promise { - if (route === 'direct') { - console.log(`[exec=direct] ${ethers.formatEther(txValue)} ${nativeSymbol}`); - return execDirect(signer, routerAddress, execCalldata, txValue); - } - // allowance-holder — persistent ERC20→AH approval except for pure native pulls - if (!cfg.isNativeInput) { - await ensureAllowanceForAllowanceHolder(signer, cfg.inputToken, inputAmount); - } - console.log(`[exec=allowance-holder] ${ethers.formatEther(txValue)} ${nativeSymbol}`); - return execViaAH( - signer, - routerAddress, - cfg.inputToken, - inputAmount, - routerAddress, - execCalldata, - txValue, - ); -} - -/** - * Runs one monolithic or modular leg for a case. - * Fetches quotes, builds calldata, and executes via {@link dispatchRouterTransaction}. - */ -async function executeLeg( - legLabel: string, - useModular: boolean, - cfg: CaseConfig, - routerAddress: string, - signer: ethers.Wallet, - signerAddress: string, - provider: ethers.JsonRpcProvider, - inputAmount: bigint, - routerIface: ethers.Interface, - routerExec: RouterExecRoute, -): Promise { - console.log(`\n── ${legLabel} (${useModular ? 'MODULAR' : 'MONOLITHIC'}) ──`); - - const nativeSymbol = cfg.sourceChainId === CHAIN_IDS.POLYGON ? 'POL' : 'ETH'; - - let inputAmountWei = inputAmount; - - let feeAmount = 0n; - let minAmountOut = 0n; - let estimatedBridgeAmount = 0n; - let ooRouter = ''; - let swapData = ''; - let ooSwapNativeWei = 0n; - let nativeFeeWithBuffer = 0n; - let amountReceivedLD = 0n; - /** Last raw quote fee (logged before buffer). */ - let nativeFeeQuoted = 0n; - - /** - * Dynamic gas reserve for native-input cases: current maxFeePerGas × generous gas limit estimate. - * Fetched once before the loop so we don't hammer the RPC on each cap iteration. - * Falls back to a hardcoded minimum if fee data is unavailable. - */ - let gasReserve = NATIVE_INPUT_GAS_RESERVE; - if (cfg.isNativeInput) { - const feeData = await provider.getFeeData(); - const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice ?? 500_000_000n; - gasReserve = maxFeePerGas * NATIVE_INPUT_GAS_LIMIT_ESTIMATE; - console.log( - ` Gas reserve (${NATIVE_INPUT_GAS_LIMIT_ESTIMATE / 1_000_000n}M gas × ${ethers.formatUnits(maxFeePerGas, 'gwei')} Gwei): ` + - `${ethers.formatEther(gasReserve)} ${nativeSymbol}`, - ); - } - - const MAX_NATIVE_INPUT_CAP_ITER = 6; - - let iter = 0; - for (;;) { - iter++; - if (iter > MAX_NATIVE_INPUT_CAP_ITER) { - throw new Error( - `${cfg.name}: native swap budgeting hit ${MAX_NATIVE_INPUT_CAP_ITER} re-quote iterations; top up native balance or widen gas reserve.`, - ); - } - - if (cfg.ooSwap !== null) { - const swapOutIsNative = cfg.ooSwap.outToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase(); - const swapOutIsUsdt0 = cfg.ooSwap.outToken.toLowerCase() === TOKENS.USDT0_POLYGON.toLowerCase(); - const swapOutLabel = swapOutIsNative - ? cfg.sourceChainId === CHAIN_IDS.POLYGON - ? 'POL' - : 'ETH' - : swapOutIsUsdt0 - ? 'USDT0' - : 'USDC'; - const swapOutDecimals = swapOutIsNative ? 18 : 6; - const fmtSwapOut = (v: bigint) => - swapOutIsNative ? ethers.formatEther(v) : ethers.formatUnits(v, swapOutDecimals); - - console.log(`Fetching OpenOcean quote (${cfg.ooSwap.inToken} → ${swapOutLabel})...`); - const q = await fetchOoQuote(cfg.ooSwap, routerAddress, inputAmountWei); - ooRouter = q.ooRouter; - swapData = q.swapData; - ooSwapNativeWei = q.nativeSwapWei; - feeAmount = bpsOf(q.estimatedOut, FEE_BPS); - estimatedBridgeAmount = q.estimatedOut - feeAmount; - minAmountOut = q.minAmountOut; - - console.log(` OO router: ${ooRouter}`); - console.log(` Est. out: ${fmtSwapOut(q.estimatedOut)} ${swapOutLabel}`); - console.log(` Fee: ${fmtSwapOut(feeAmount)} ${swapOutLabel} (${FEE_BPS} bps)`); - console.log(` Min out: ${fmtSwapOut(minAmountOut)} ${swapOutLabel}`); - if (ooSwapNativeWei > 0n) { - console.log(` OO swap value wei: ${ooSwapNativeWei.toString()} (attached to OO call)`); - } - } else { - // Case 2: no OO swap — bridge entire balance minus fee - feeAmount = bpsOf(inputAmountWei, FEE_BPS); - estimatedBridgeAmount = inputAmountWei - feeAmount; - console.log(` Fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); - } - - console.log(`Fetching bridge quoteSend (${cfg.bridgeContract}, extraOpts ${cfg.lzExtraOptions.slice(0, 18)}...) ...`); - const lzQuote = await fetchStargateQuote( - cfg.bridgeContract, - provider, - cfg.destLzEid, - signerAddress, - estimatedBridgeAmount, - cfg.lzExtraOptions, - ); - nativeFeeQuoted = lzQuote.nativeFee; - nativeFeeWithBuffer = (lzQuote.nativeFee * 105n) / 100n; - amountReceivedLD = lzQuote.amountReceivedLD; - - console.log(` nativeFee: ${ethers.formatEther(nativeFeeQuoted)} ${nativeSymbol}`); - console.log(` nativeFee +5%buf: ${ethers.formatEther(nativeFeeWithBuffer)} ${nativeSymbol}`); - console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, cfg.isNativePool ? 18 : 6)}`); - - if (!cfg.isNativeInput) { - break; - } - - const balNow = await provider.getBalance(signerAddress); - // maxAffordableSwapIn = balance we can put into the swap leg so that - // txValue (= inputAmountWei + nativeFeeWithBuffer) + gas cost ≤ balance - const maxAffordableSwapIn = balNow - nativeFeeWithBuffer - gasReserve; - if (maxAffordableSwapIn <= 0n) { - throw new Error( - `${cfg.name}: signer native balance (${ethers.formatEther(balNow)} ${nativeSymbol}) cannot cover lz fee ` + - `(${ethers.formatEther(nativeFeeWithBuffer)} ${nativeSymbol}) plus gas reserve ` + - `(${ethers.formatEther(gasReserve)} ${nativeSymbol}).`, - ); - } - if (inputAmountWei <= maxAffordableSwapIn) { - break; - } - - console.warn( - `[${legLabel}] capping ${nativeSymbol} swap input: planned ${ethers.formatEther(inputAmountWei)} ` + - `exceeds max affordable ${ethers.formatEther(maxAffordableSwapIn)} (balance − lz fee − gas reserve). Re-quoting.`, - ); - inputAmountWei = maxAffordableSwapIn; - } - - // ── State prep for reproducible gas ───────────────────────────────────────── - // Determine the token the router will approve to the bridge contract (null for native pools). - const bridgeToken: string | null = - cfg.isNativePool - ? null - : cfg.ooSwap !== null - ? cfg.ooSwap.outToken - : cfg.inputToken; - - if (!cfg.isNativeInput) { - await ensureRouterErc20Balance(signer, cfg.inputToken, routerAddress); - } - if (cfg.isNativeInput || cfg.isNativePool || (cfg.ooSwap && cfg.ooSwap.outToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase())) { - await ensureRouterNativeBalance(signer, routerAddress); - } - if (cfg.ooSwap && cfg.ooSwap.outToken.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) { - await ensureRouterErc20Balance(signer, cfg.ooSwap.outToken, routerAddress); - } - if (cfg.ooSwap && !cfg.isNativeInput) { - await ensureRouterApproval(signer, routerAddress, cfg.inputToken, ooRouter); - } - if (bridgeToken && bridgeToken.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) { - await ensureRouterApproval(signer, routerAddress, bridgeToken, cfg.bridgeContract); - } - // ──────────────────────────────────────────────────────────────────────────── - - // Native pool: use estimatedBridgeAmount as placeholder; router splices actual finalETH at runtime. - // ERC20 pool: 0n placeholder; router splices actual post-fee balance at runtime. - const amountLD = cfg.isNativePool ? estimatedBridgeAmount : 0n; - - const stargateData = buildStargateCalldata(cfg.destLzEid, nativeFeeWithBuffer, signerAddress, amountLD, cfg.lzExtraOptions); - - let execCalldata: string; - if (useModular) { - let actions: ModularAction[]; - if (cfg.isNativePool) { - actions = buildNativePoolModularActions( - signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, - minAmountOut, ooRouter, swapData, stargateData, - ); - } else if (cfg.isNativeInput) { - actions = buildNativeInErc20BridgeModularActions( - signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, - ooRouter, swapData, stargateData, ooSwapNativeWei, - ); - } else { - actions = buildErc20PoolModularActions( - signerAddress, routerAddress, cfg, inputAmountWei, feeAmount, nativeFeeWithBuffer, stargateData, - ); - } - execCalldata = routerIface.encodeFunctionData('performModularExecution', [ZERO_BYTES32, actions]); - } else { - let mono: MonolithicExecutionCall; - if (cfg.isNativePool) { - mono = buildNativePoolMonolithic( - signerAddress, cfg, inputAmountWei, feeAmount, minAmountOut, - ooRouter, swapData, stargateData, nativeFeeWithBuffer, - ); - } else if (cfg.isNativeInput) { - mono = buildNativeInErc20BridgeMonolithic( - signerAddress, cfg, inputAmountWei, feeAmount, minAmountOut, - ooRouter, swapData, stargateData, nativeFeeWithBuffer, ooSwapNativeWei, - ); - } else { - mono = buildErc20PoolMonolithic( - signerAddress, cfg, inputAmountWei, feeAmount, stargateData, nativeFeeWithBuffer, - ); - } - execCalldata = routerIface.encodeFunctionData('performExecution', monolithicArgs(mono)); - } - - const txValue = cfg.isNativeInput ? inputAmountWei + nativeFeeWithBuffer : nativeFeeWithBuffer; - - const receipt = await dispatchRouterTransaction( - routerExec, - cfg, - signer, - routerAddress, - execCalldata, - inputAmountWei, - txValue, - nativeSymbol, - ); - - logTxnSummary(`${cfg.name} — ${useModular ? 'Modular' : 'Monolithic'}`, cfg.sourceChainId, receipt); -} - -// ─── Run one case (monolithic + sleep + modular) ────────────────────────────── - -async function runCase( - cfg: CaseConfig, - signer: ethers.Wallet, - signerAddress: string, - routerIface: ethers.Interface, - routerExec: RouterExecRoute, -): Promise { - const routerAddress = routerAddressForChain(cfg.sourceChainId); - console.log(`\n${'═'.repeat(70)}`); - console.log(`CASE: ${cfg.name}`); - console.log('═'.repeat(70)); - console.log(`Router (chain ${cfg.sourceChainId}): ${routerAddress}`); - console.log(`Router exec route: ${routerExec}`); - - const provider = new ethers.JsonRpcProvider(cfg.rpc); - const signerOnChain = signer.connect(provider); - - let walletBalance: bigint; - let decimals: number; - if (cfg.isNativeInput) { - const raw = await provider.getBalance(signerAddress); - if (raw <= NATIVE_INPUT_GAS_RESERVE) { - const sym = cfg.sourceChainId === CHAIN_IDS.POLYGON ? 'POL' : 'ETH'; - throw new Error( - `${cfg.name}: native balance ${ethers.formatEther(raw)} ${sym} is below reserve of ${ethers.formatEther(NATIVE_INPUT_GAS_RESERVE)} ${sym}.`, - ); - } - // Reserve wei for signer gas; lz fee itself is deducted inside executeLeg (`txValue = swap + fee`). - walletBalance = raw - NATIVE_INPUT_GAS_RESERVE - 20n; - decimals = 18; - } else { - ({ balance: walletBalance, decimals } = await getWalletErc20Balance( - cfg.inputToken, - signerAddress, - provider, - )); - } - if (walletBalance === 0n) { - throw new Error( - `${cfg.name}: signer ${signerAddress} has zero usable balance of ${cfg.inputToken} on chain ${cfg.sourceChainId}.`, - ); - } - - const legAmount = cfg.isNativeInput ? walletBalance / 2n : (walletBalance - 20n) / 2n; - if (legAmount === 0n) { - throw new Error(`${cfg.name}: balance too small to split into two halves.`); - } - - console.log(`Input token balance: ${ethers.formatUnits(walletBalance, decimals)} (${cfg.inputToken})`); - console.log(`Per leg (½): ${ethers.formatUnits(legAmount, decimals)}`); - - await executeLeg('1/2', false, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface, routerExec); - - console.log('\nSleeping 3s before modular leg...'); - await sleep(3000); - - await executeLeg('2/2', true, cfg, routerAddress, signerOnChain, signerAddress, provider, legAmount, routerIface, routerExec); -} - -// ─── Main ───────────────────────────────────────────────────────────────────── - -async function main() { - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY env var required'); - } - - const cfg = resolveScenarioConfig(); - const routerExec = resolveRouterExecRoute(cfg); - - // Use any provider to create the wallet; the case reconnects via `runCase`. - const signer = new ethers.Wallet(privateKey); - const signerAddress = await signer.getAddress(); - const routerIface = new ethers.Interface(ROUTER_ABI); - - console.log(`Signer: ${signerAddress}`); - console.log(`Router: ${routerAddressForChain(cfg.sourceChainId)} (chain ${cfg.sourceChainId})`); - console.log(`Scenario: ${process.argv[2] ?? process.env.STARGATE_E2E_CASE ?? '(resolved)'}`); - console.log(`Exec: ${routerExec} (argv[3] overrides STARGATE_ROUTER_EXEC; required for native input)`); - - await runCase(cfg, signer, signerAddress, routerIface, routerExec); - - console.log('\n✓ Stargate case completed.'); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/e2e/utils/allowanceHolder.ts b/scripts/e2e/utils/allowanceHolder.ts index 3119ba7..aab6a1e 100644 --- a/scripts/e2e/utils/allowanceHolder.ts +++ b/scripts/e2e/utils/allowanceHolder.ts @@ -114,11 +114,11 @@ export async function execViaAH( * AllowanceHolder. Use this when the input token is native ETH/POL — the router's * `_pullFromUser` path for native tokens only checks `msg.value >= amount` and does * NOT enforce `_msgSender() == user` nor call `AH.transferFrom`. For modular - * execution (`performModularExecution`) there is no `_pullFromUser` at all. + * execution (`performActions`) there is no `_pullFromUser` at all. * * @param signer - EOA signing and paying for the tx * @param target - Router contract address - * @param callData - Encoded `performExecution` or `performModularExecution` calldata + * @param callData - Encoded router entrypoint calldata (`swap`, `bridge`, `performActions`, etc.) * @param txValue - ETH to forward (inputAmount + nativeFeeWithBuffer for native input) */ export async function execDirect( diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index 9f5dfac..a55c4cb 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -1,11 +1,8 @@ /** - * TypeScript interfaces that mirror every Solidity struct in - * Combined unchecked router. The order and field names must match the ABI - * produced by the compiler so that ethers.js can encode them correctly. + * TypeScript interfaces mirroring BungeeOpenRouter Solidity structs. + * Field names and order must match the compiler ABI encoding. */ -// ─── Monolithic execution types ─────────────────────────────────────────────── - export interface InputData { user: string; inputToken: string; @@ -32,30 +29,22 @@ export interface BridgeData { value: bigint; } -export interface MonolithicExecution { - input: InputData; - preFee: FeeData; - swap: SwapData; - postFee: FeeData; - bridge: BridgeData; - flags: bigint; -} - -export interface MonolithicExecutionCall { - exec: MonolithicExecution; - swapCallData: string; - bridgeCallData: string; -} - -export const BRIDGE_VALUE_FLAG = 4n; -export const BRIDGE_AMOUNT_POSITION_FLAG = 8n; +export const POST_FEE_FLAG = 0x01n; +export const BALANCE_FLAG = 0x02n; +export const BRIDGE_VALUE_FLAG = 0x04n; +export const BRIDGE_AMOUNT_POSITION_FLAG = 0x08n; export const BRIDGE_AMOUNT_POSITION_SHIFT = 16n; export const MAX_BRIDGE_AMOUNT_POSITION = 0xffffn; -/** 32-byte zero; use as `requestHash` when scripts do not assign a request id. */ +/** 32-byte zero; use as `quoteId` when scripts do not assign a correlation id. */ export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' as const; +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +/** Convenience: empty fee (no fee taken) */ +export const NO_FEE: FeeData = { receiver: ZERO_ADDRESS, amount: 0n }; + export function bridgeAmountPositionFlag(position: bigint | number): bigint { const positionBigInt = BigInt(position); if (positionBigInt < 0n || positionBigInt > MAX_BRIDGE_AMOUNT_POSITION) { @@ -64,26 +53,53 @@ export function bridgeAmountPositionFlag(position: bigint | number): bigint { return BRIDGE_AMOUNT_POSITION_FLAG | (positionBigInt << BRIDGE_AMOUNT_POSITION_SHIFT); } -export function monolithicArgs( - call: MonolithicExecutionCall, - requestHash: string = ZERO_BYTES32, -): readonly [string, MonolithicExecution, string, string] { - return [requestHash, call.exec, call.swapCallData, call.bridgeCallData] as const; +export function swapArgs( + quoteId: string, + flags: bigint, + input: InputData, + fee: FeeData, + swapData: SwapData, + swapCallData: string, + receiver: string, +): readonly [string, bigint, InputData, FeeData, SwapData, string, string] { + return [quoteId, flags, input, fee, swapData, swapCallData, receiver] as const; } -// ─── Sentinel / zero helpers ────────────────────────────────────────────────── - -export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; +export function swapAndBridgeArgs( + quoteId: string, + flags: bigint, + input: InputData, + fee: FeeData, + swapData: SwapData, + swapCallData: string, + bridgeData: BridgeData, + bridgeCallData: string, +): readonly [ + string, + bigint, + InputData, + FeeData, + SwapData, + string, + BridgeData, + string, +] { + return [quoteId, flags, input, fee, swapData, swapCallData, bridgeData, bridgeCallData] as const; +} -/** Convenience: empty fee (no fee taken) */ -export const NO_FEE: FeeData = { receiver: ZERO_ADDRESS, amount: 0n }; +export function bridgeArgs( + quoteId: string, + input: InputData, + fee: FeeData, + bridgeData: BridgeData, + bridgeCallData: string, +): readonly [string, InputData, FeeData, BridgeData, string] { + return [quoteId, input, fee, bridgeData, bridgeCallData] as const; +} -/** Convenience: empty swap (skip swap step) */ -export const NO_SWAP: SwapData = { - target: ZERO_ADDRESS, - approvalSpender: ZERO_ADDRESS, - outputToken: ZERO_ADDRESS, - value: 0n, - minOutput: 0n, - returnDataWordOffset: 0n, -}; +export function performActionsArgs( + quoteId: string, + actions: { actionInfo: bigint | string; data: string; splices: (bigint | string)[] }[], +): readonly [string, typeof actions] { + return [quoteId, actions] as const; +} diff --git a/scripts/e2e/utils/modularActionsBuilder/README.md b/scripts/e2e/utils/modularActionsBuilder/README.md index 81211e3..3768bc6 100644 --- a/scripts/e2e/utils/modularActionsBuilder/README.md +++ b/scripts/e2e/utils/modularActionsBuilder/README.md @@ -1,6 +1,6 @@ # Modular Actions Builder -Dependency-free helper for formatting packed `performModularExecution(Action[])` +Dependency-free helper for formatting packed `performActions(Action[])` payloads from provider SDK/API calldata. ```js @@ -102,4 +102,4 @@ exec Use `toActions()` when the caller already has an ABI encoder for the packed modular action tuple. Use `toLogicalActions()` for the readable builder shape. Use `toCalldata()` when you need raw -`performModularExecution(Action[])` calldata. +`performActions(Action[])` calldata. diff --git a/scripts/e2e/utils/modularActionsBuilder/index.d.ts b/scripts/e2e/utils/modularActionsBuilder/index.d.ts index 06b6121..22a492e 100644 --- a/scripts/e2e/utils/modularActionsBuilder/index.d.ts +++ b/scripts/e2e/utils/modularActionsBuilder/index.d.ts @@ -35,7 +35,9 @@ export interface ModularAction { export type Action = LogicalAction; -export declare const PERFORM_MODULAR_EXECUTION_SELECTOR: "0x4f85c3a5"; +export declare const PERFORM_ACTIONS_SELECTOR: "0x197aa51e"; +/** @deprecated Use PERFORM_ACTIONS_SELECTOR */ +export declare const PERFORM_MODULAR_EXECUTION_SELECTOR: "0x197aa51e"; export declare const CallType: Readonly<{ CALL: 0; @@ -97,6 +99,8 @@ export declare class ActionRef { } export declare function concatHex(values: Hex[]): Hex; +export declare function encodePerformActionsArgs(actions: Array): Hex; +/** @deprecated Use encodePerformActionsArgs */ export declare function encodePerformModularExecutionArgs(actions: Array): Hex; export declare function encodeWord(value: BigNumberish): Hex; export declare function packActionInfo(action: Pick): bigint; diff --git a/scripts/e2e/utils/modularActionsBuilder/index.js b/scripts/e2e/utils/modularActionsBuilder/index.js index b0ba0d2..4c52612 100644 --- a/scripts/e2e/utils/modularActionsBuilder/index.js +++ b/scripts/e2e/utils/modularActionsBuilder/index.js @@ -1,6 +1,8 @@ "use strict"; -const PERFORM_MODULAR_EXECUTION_SELECTOR = "0x4f85c3a5"; +const PERFORM_ACTIONS_SELECTOR = "0x197aa51e"; +/** @deprecated Use PERFORM_ACTIONS_SELECTOR */ +const PERFORM_MODULAR_EXECUTION_SELECTOR = PERFORM_ACTIONS_SELECTOR; const WORD_BYTES = 32; const WORD_HEX_CHARS = WORD_BYTES * 2; const UINT256_MAX = (1n << 256n) - 1n; @@ -108,7 +110,7 @@ class ModularActionsBuilder { toCalldata() { this._markSpliceSources(); - return concatHex([PERFORM_MODULAR_EXECUTION_SELECTOR, encodePerformModularExecutionArgs(this._actions)]); + return concatHex([PERFORM_ACTIONS_SELECTOR, encodePerformActionsArgs(this._actions)]); } _label(index, label) { @@ -225,10 +227,15 @@ class ActionRef { } } -function encodePerformModularExecutionArgs(actions) { +function encodePerformActionsArgs(actions) { return concatHex([encodeWord(WORD_BYTES), encodeActionArray(prepareActionsForEncoding(actions))]); } +/** @deprecated Use encodePerformActionsArgs */ +function encodePerformModularExecutionArgs(actions) { + return encodePerformActionsArgs(actions); +} + function encodeActionArray(actions) { const encodedActions = actions.map(encodeActionTuple); let nextOffset = WORD_BYTES * actions.length; @@ -434,10 +441,12 @@ module.exports = { ActionHandle, ActionRef, CallType, + PERFORM_ACTIONS_SELECTOR, PERFORM_MODULAR_EXECUTION_SELECTOR, Offset, ModularActionsBuilder, concatHex, + encodePerformActionsArgs, encodePerformModularExecutionArgs, encodeWord, packActionInfo, diff --git a/scripts/e2e/utils/reproducibility.ts b/scripts/e2e/utils/reproducibility.ts index 9db88d0..6e34f9f 100644 --- a/scripts/e2e/utils/reproducibility.ts +++ b/scripts/e2e/utils/reproducibility.ts @@ -6,24 +6,21 @@ * * Before each test leg these ensure: * 1. The router holds ≥ 20 wei of every token whose balance slot will be written. - * 2. The router has a non-zero ERC-20 allowance for every external spender it will call. * - * Seeding slots to non-zero means subsequent SSTORE writes cost ~2 900 gas + * Router→spender ERC-20 approvals are NOT pre-seeded here. `BungeeOpenRouter` + * sets max allowance inside `swap`, `bridge`, and `swapAndBridge` when needed. + * Modular `performActions` legs may still include inline `approve` actions in the + * same transaction when testing raw modular flows. + * + * Seeding balance slots to non-zero means subsequent SSTORE writes cost ~2 900 gas * (non-zero → non-zero) rather than ~20 000 gas (zero → non-zero), giving * consistent gas readings across repeated runs. */ import { ethers } from 'ethers'; -import { getErc20Contract, encodeApprove } from './erc20'; -import { execViaAH } from './allowanceHolder'; -import { ROUTER_ABI } from './routerAbi'; -import { ZERO_BYTES32 } from './contractTypes'; +import { getErc20Contract } from './erc20'; const SEED_WEI = 20n; -function packCallAction(target: string): bigint { - return BigInt(target) << 16n; // CallType.CALL=0, storeResult=false -} - /** * Transfers {@link SEED_WEI} of `token` from `signer` to the deployed open router only * when that router already holds zero — never to Relay/deposit/spender contracts. @@ -70,43 +67,16 @@ export async function ensureRouterNativeBalance( } /** - * Issues `token.approve(spender, MaxUint256)` FROM the router (via - * `performModularExecution`) when the current router→spender allowance is zero. + * No-op: router→spender approvals are handled by the contract on `swap` / + * `bridge` / `swapAndBridge`. Kept so existing e2e scripts do not need rewrites. * - * Guarantees the allowance slot is non-zero before the test txn so that the - * approval write inside the test costs ~2 900 gas (non-zero → non-zero). + * @deprecated Pre-approval via a separate `performActions` tx is intentionally disabled. */ export async function ensureRouterApproval( - signer: ethers.Wallet, - openRouterAddress: string, - token: string, - spender: string, + _signer: ethers.Wallet, + _openRouterAddress: string, + _token: string, + _spender: string, ): Promise { - const openRouter = ethers.getAddress(openRouterAddress); - const tokenResolved = ethers.getAddress(token); - const spenderResolved = ethers.getAddress(spender); - const tokenRo = getErc20Contract(tokenResolved, signer.provider!); - const allowance = BigInt(await tokenRo.allowance(openRouter, spenderResolved)); - if (allowance > 0n) { - return; - } - - console.log( - ` [state-prep] open router ${openRouter} token ${tokenResolved} allowance for ${spenderResolved}=0 — pre-approving MaxUint256 via open router`, - ); - const routerIface = new ethers.Interface(ROUTER_ABI); - const actions = [ - { - actionInfo: packCallAction(tokenResolved), - data: encodeApprove(spenderResolved, ethers.MaxUint256), - splices: [], - }, - ]; - const calldata = routerIface.encodeFunctionData('performModularExecution', [ - ZERO_BYTES32, - actions, - ]); - // Route through AllowanceHolder so _msgSender() resolves correctly inside the router. - // amount=0 because we are not pulling user tokens — we only need AH to forward the call. - await execViaAH(signer, openRouter, tokenResolved, 0n, openRouter, calldata); + // Intentionally empty — see module header. } diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index 2caa0e0..03dc419 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -1,45 +1,27 @@ /** - * ABI fragment for the combined unchecked router — only the two entrypoints - * called from e2e scripts. Structs must exactly match the Solidity definitions. + * ABI fragments for BungeeOpenRouter entrypoints used by e2e scripts. + * Struct field order must match the Solidity definitions. */ export const ROUTER_ABI = [ - // Monolithic path — `requestHash` is first for indexer-friendly calldata layout - `function performExecution( - bytes32 requestHash, - ( - (address user, address inputToken, uint256 inputAmount) input, - (address receiver, uint256 amount) preFee, - (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swap, - (address receiver, uint256 amount) postFee, - (address target, address approvalSpender, uint256 value) bridge, - uint256 flags - ) exec, - bytes swapCallData, - bytes bridgeCallData - ) external payable`, - - // Modular path - `function performModularExecution( - bytes32 requestHash, + `function performActions( + bytes32 quoteId, (uint256 actionInfo, bytes data, uint256[] splices)[] actions ) external payable`, - // Standalone swap — pull, optional fee, swap; returns finalAmount `function swap( - bytes32 requestHash, - (address user, address inputToken, uint256 inputAmount) input, - address receiver, + bytes32 quoteId, uint256 flags, + (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) fee, (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, - bytes swapCallData + bytes swapCallData, + address receiver ) external payable returns (uint256)`, - // Swap + bridge — pull, optional fee, swap, then bridge with optional amount splicing `function swapAndBridge( - bytes32 requestHash, - (address user, address inputToken, uint256 inputAmount) input, + bytes32 quoteId, uint256 flags, + (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) fee, (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, bytes swapCallData, @@ -47,9 +29,8 @@ export const ROUTER_ABI = [ bytes bridgeCallData ) external payable`, - // Simple bridge path (no swap, no splicing — caller pre-encodes finalAmount into data) `function bridge( - bytes32 requestHash, + bytes32 quoteId, (address user, address inputToken, uint256 inputAmount) input, (address receiver, uint256 amount) fee, (address target, address approvalSpender, uint256 value) bridgeData, diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 82e7f61..8065379 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -41,6 +41,7 @@ contract OneInchCctpOpenRouterPoCTest is Test { uint32 internal constant BASE_CCTP_DOMAIN = 6; uint256 internal constant CCTP_MAX_FEE = 0x2710; uint32 internal constant CCTP_MIN_FINALITY_THRESHOLD = 1000; + string internal constant SOCKET_GATEWAY_REFERENCE_CALLDATA_PREFIX = "0x000001ad4db9cf6a00000000000000000000000000000000000000000000000000000000000001a60000000000000000000000000000000000000000000000000000000000000120000000000000000000000000b0bbff6311b7f245761A7846d3Ce7B1b100C1836000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000021050000000000000000000000000000000000000000000000000000000000007530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000013e4ee8f0b86000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000000000000000000000002d169fe80174000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000001304"; string internal constant SOCKET_GATEWAY_REFERENCE_CALLDATA_SUFFIX = @@ -91,7 +92,7 @@ contract OneInchCctpOpenRouterPoCTest is Test { _assertPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); } - function test_oneInchSwapCctpBridgeMonolithic_polygonFork() public { + function test_oneInchSwapCctpBridgeSwapAndBridge_polygonFork() public { string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); if (bytes(rpcUrl).length != 0) { uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); @@ -127,7 +128,7 @@ contract OneInchCctpOpenRouterPoCTest is Test { uint256 executeGasUsed = gasBeforeExecute - gasleft(); emit log_named_uint("AllowanceHolder.exec -> router.swapAndBridge gas used", executeGasUsed); - _assertMonolithicPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); + _assertPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); } function test_oneInchSwapCctpBridgeSocketGatewayReference_polygonFork() public { @@ -262,17 +263,6 @@ contract OneInchCctpOpenRouterPoCTest is Test { assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0); } - function _assertMonolithicPocResult(Router router, uint256 feeRecipientUsdcBefore, uint256 usdcSupplyBefore) - internal - view - { - assertEq(ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT) - feeRecipientUsdcBefore, ROUTE_FEE_USDC); - assertEq(ERC20(POLYGON_USDC).totalSupply(), usdcSupplyBefore - EXPECTED_CCTP_BURN_AMOUNT); - assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_RECIPIENT), 0); - assertEq(ERC20(POLYGON_AAVE).balanceOf(address(router)), 0); - assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0); - } - function _assertSocketGatewayPocResult(uint256 feeRecipientUsdcBefore, uint256 usdcSupplyBefore) internal view { assertEq( ERC20(POLYGON_AAVE).allowance(FIXTURE_RECIPIENT, FIXTURE_ROUTER), REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 74e951b..691ae10 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -38,6 +38,7 @@ contract OpenOceanAcrossOpenRouterPoCTest is Test { uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01d6d1; uint256 internal constant SWAP_INPUT_USDC = 0x1640325; uint256 internal constant DEFAULT_ACROSS_BRIDGE_FEE = 1; + string internal constant OPENOCEAN_SWAP_CALLDATA = "0x0a9704d5000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a20000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000001640325000000000000000000000000000000000000000000000000002002d5154237f3000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b94700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001640325000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab100000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e8500000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef00020000000000000000000000000000000000000000000000239364a56cb36600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f9900000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; From 5a2b25ac65932272840cb0736b2cab2ada9d19aa Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 20:29:38 +0530 Subject: [PATCH 59/69] test: set approvalSpender zero if enough allowance --- .../e2e/arbitrum/performExecution.postFee.ts | 14 ++- .../performModularExecution.postFee.ts | 8 +- scripts/e2e/cctp/bridge.preFee.ts | 14 ++- scripts/e2e/cctp/performExecution.postFee.ts | 24 +++- scripts/e2e/cctp/performExecution.preFee.ts | 14 ++- .../cctp/performModularExecution.postFee.ts | 19 ++- .../cctp/performModularExecution.preFee.ts | 17 ++- .../cctp/swapAndBridge.postFee.balanceOf.ts | 23 ++-- ...pAndBridge.postFee.returndata.kyberswap.ts | 23 ++-- .../cctp/swapAndBridge.postFee.returndata.ts | 23 ++-- .../cctp/swapAndBridge.preFee.balanceOf.ts | 23 ++-- .../cctp/swapAndBridge.preFee.returndata.ts | 23 ++-- scripts/e2e/config.ts | 2 +- scripts/e2e/oft/bridge.preFee.ts | 14 ++- scripts/e2e/oft/performExecution.postFee.ts | 23 ++-- scripts/e2e/oft/performExecution.preFee.ts | 14 ++- .../oft/performModularExecution.postFee.ts | 19 ++- .../e2e/oft/performModularExecution.preFee.ts | 16 ++- .../oft/swapAndBridge.postFee.balanceOf.ts | 23 ++-- .../oft/swapAndBridge.postFee.returndata.ts | 23 ++-- .../e2e/oft/swapAndBridge.preFee.balanceOf.ts | 23 ++-- .../oft/swapAndBridge.preFee.returndata.ts | 23 ++-- scripts/e2e/relay/aave.bridge.preFee.ts | 14 ++- .../e2e/relay/aave.performExecution.preFee.ts | 14 ++- .../aave.performModularExecution.preFee.ts | 8 +- scripts/e2e/relay/usdc.bridge.preFee.ts | 14 ++- .../e2e/relay/usdc.performExecution.preFee.ts | 14 ++- .../usdc.performModularExecution.preFee.ts | 8 +- ...arbUsdcBaseEth.performExecution.postFee.ts | 14 ++- ...BaseEth.performModularExecution.postFee.ts | 8 +- ...baseUsdcArbEth.performExecution.postFee.ts | 15 ++- ...cArbEth.performModularExecution.postFee.ts | 8 +- ...gonPolUsdt0Arb.performExecution.postFee.ts | 14 ++- ...sdt0Arb.performModularExecution.postFee.ts | 16 ++- .../stargate/polygonUsdcBase.bridge.preFee.ts | 14 ++- ...olygonUsdcBase.performExecution.postFee.ts | 14 ++- ...sdcBase.performModularExecution.postFee.ts | 16 ++- .../swapAndBridge.postFee.balanceOf.ts | 15 ++- .../swapAndBridge.postFee.returndata.ts | 15 ++- .../swapAndBridge.preFee.balanceOf.ts | 15 ++- .../swapAndBridge.preFee.returndata.ts | 15 ++- .../e2e/swap/kyberswap.postFee.balanceOf.ts | 12 +- .../e2e/swap/kyberswap.postFee.returndata.ts | 12 +- .../e2e/swap/kyberswap.preFee.balanceOf.ts | 12 +- .../e2e/swap/kyberswap.preFee.returndata.ts | 12 +- scripts/e2e/swap/swap.postFee.balanceOf.ts | 14 ++- scripts/e2e/swap/swap.postFee.returndata.ts | 14 ++- scripts/e2e/swap/swap.preFee.balanceOf.ts | 14 ++- scripts/e2e/swap/swap.preFee.returndata.ts | 14 ++- scripts/e2e/swap/zerox.postFee.balanceOf.ts | 12 +- scripts/e2e/swap/zerox.postFee.returndata.ts | 12 +- scripts/e2e/swap/zerox.preFee.balanceOf.ts | 13 ++- scripts/e2e/swap/zerox.preFee.returndata.ts | 14 +-- scripts/e2e/utils/reproducibility.ts | 22 +--- scripts/e2e/utils/routerAllowance.ts | 110 ++++++++++++++++++ 55 files changed, 644 insertions(+), 299 deletions(-) create mode 100644 scripts/e2e/utils/routerAllowance.ts diff --git a/scripts/e2e/arbitrum/performExecution.postFee.ts b/scripts/e2e/arbitrum/performExecution.postFee.ts index 4c009f1..3e97d1e 100644 --- a/scripts/e2e/arbitrum/performExecution.postFee.ts +++ b/scripts/e2e/arbitrum/performExecution.postFee.ts @@ -36,7 +36,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const FLAGS = POST_FEE_FLAG | BRIDGE_VALUE_FLAG; const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); @@ -108,7 +109,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); await ensureRouterNativeBalance(signer, ROUTER_ETH); - await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_ETH, + TOKENS.AAVE_ETH, + ooRouter, + inputAmount, + ); const callData = routerIface.encodeFunctionData( 'swapAndBridge', @@ -119,7 +127,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/arbitrum/performModularExecution.postFee.ts b/scripts/e2e/arbitrum/performModularExecution.postFee.ts index 78a0dcf..069e29d 100644 --- a/scripts/e2e/arbitrum/performModularExecution.postFee.ts +++ b/scripts/e2e/arbitrum/performModularExecution.postFee.ts @@ -34,12 +34,13 @@ import { NATIVE_TOKEN_ADDRESS, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); @@ -133,14 +134,13 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); await ensureRouterNativeBalance(signer, ROUTER_ETH); - await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_ETH, signerAddress, ROUTER_ETH, inputAmount])); - exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouter, inputAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter, inputAmount, inputAmount); exec.call(ooRouter, swapData); exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); diff --git a/scripts/e2e/cctp/bridge.preFee.ts b/scripts/e2e/cctp/bridge.preFee.ts index ce85682..bbac9a1 100644 --- a/scripts/e2e/cctp/bridge.preFee.ts +++ b/scripts/e2e/cctp/bridge.preFee.ts @@ -27,7 +27,8 @@ import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -93,15 +94,22 @@ async function main(): Promise { bridgeAmount, ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + polyCctp.tokenMessenger, + bridgeAmount, + ); + const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }; + const bridgeData: BridgeData = { target: polyCctp.tokenMessenger, approvalSpender: bridgeApprovalSpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, polyCctp.tokenMessenger); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/cctp/performExecution.postFee.ts b/scripts/e2e/cctp/performExecution.postFee.ts index 645346a..36d0a50 100644 --- a/scripts/e2e/cctp/performExecution.postFee.ts +++ b/scripts/e2e/cctp/performExecution.postFee.ts @@ -32,7 +32,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(4); const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -121,8 +122,21 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ); const callData = routerIface.encodeFunctionData( 'swapAndBridge', @@ -133,14 +147,14 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, returnDataWordOffset: 0n, }, swapData, - { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, + { target: polyCctp.tokenMessenger, approvalSpender: bridgeApprovalSpender, value: 0n }, depositForBurnData, ), ); diff --git a/scripts/e2e/cctp/performExecution.preFee.ts b/scripts/e2e/cctp/performExecution.preFee.ts index 5065564..cc53631 100644 --- a/scripts/e2e/cctp/performExecution.preFee.ts +++ b/scripts/e2e/cctp/performExecution.preFee.ts @@ -30,7 +30,8 @@ import { type InputData, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -88,14 +89,21 @@ async function main() { const input: InputData = { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + bridgeAmount, + ); + const bridgeData: BridgeData = { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }; await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); const routerIface = new ethers.Interface(ROUTER_ABI); const callData = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositForBurnData)); diff --git a/scripts/e2e/cctp/performModularExecution.postFee.ts b/scripts/e2e/cctp/performModularExecution.postFee.ts index 7d37635..c040a37 100644 --- a/scripts/e2e/cctp/performModularExecution.postFee.ts +++ b/scripts/e2e/cctp/performModularExecution.postFee.ts @@ -33,12 +33,13 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -126,18 +127,24 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, inputAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter, inputAmount, inputAmount); exec.call(ooRouter, swapData); exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(polyCctp.tokenMessenger, ethers.MaxUint256)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ethers.MaxUint256, + ); const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); diff --git a/scripts/e2e/cctp/performModularExecution.preFee.ts b/scripts/e2e/cctp/performModularExecution.preFee.ts index 89582a2..163391f 100644 --- a/scripts/e2e/cctp/performModularExecution.preFee.ts +++ b/scripts/e2e/cctp/performModularExecution.preFee.ts @@ -28,12 +28,13 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -71,6 +72,7 @@ async function main() { if (inputAmount === 0n) throw new Error('Balance too small'); const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; console.log(`Signer: ${signerAddress}`); console.log(`Router: ${ROUTER_POLYGON}`); @@ -85,7 +87,6 @@ async function main() { const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -93,7 +94,15 @@ async function main() { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_POLYGON_CIRCLE, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(polyCctp.tokenMessenger, ethers.MaxUint256)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + bridgeAmount, + ethers.MaxUint256, + ); const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts index 1a7fe75..243e0a7 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) | bridge amount at byte offset 4 (depositForBurn amount param) const FLAGS = 0x03n | bridgeAmountPositionFlag(4); @@ -149,17 +149,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + estimatedOut - feeAmount, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -173,7 +176,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -182,7 +185,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts index 3a8abf6..0d9c5b0 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge amount at byte offset 4 (depositForBurn amount param) const FLAGS = 0x01n | bridgeAmountPositionFlag(4); @@ -203,17 +203,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ksRouter + ksRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + estimatedOut - feeAmount, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -227,7 +230,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, @@ -236,7 +239,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts index dffa1df..415e714 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge amount at byte offset 4 (depositForBurn amount param) const FLAGS = 0x01n | bridgeAmountPositionFlag(4); @@ -149,17 +149,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + estimatedOut - feeAmount, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -173,7 +176,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -182,7 +185,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts index 7d0b75c..1e2449a 100644 --- a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) | bridge amount at byte offset 4 (depositForBurn amount param) const FLAGS = 0x02n | bridgeAmountPositionFlag(4); @@ -151,17 +151,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + swapInputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + minAmountOut, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -175,7 +178,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -184,7 +187,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts index f9729ac..72050de 100644 --- a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | bridge amount at byte offset 4 (depositForBurn amount param) const FLAGS = bridgeAmountPositionFlag(4); @@ -151,17 +151,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + swapInputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + minAmountOut, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -175,7 +178,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -184,7 +187,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 24f4dc6..2a25808 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -34,7 +34,7 @@ export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. */ export const ROUTER_BY_CHAIN_ID: Record = { - [CHAIN_IDS.POLYGON]: '0x7894c2c93e8952867e2fA4C0778296fEE77074Ea', + [CHAIN_IDS.POLYGON]: '0x33654252CEA9c95220Aa1d434a3631d5c0843AA4', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', [CHAIN_IDS.BASE]: '0x91b536E79cd3607b593f3011937862609D608253', [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', diff --git a/scripts/e2e/oft/bridge.preFee.ts b/scripts/e2e/oft/bridge.preFee.ts index 49a909f..756ed5f 100644 --- a/scripts/e2e/oft/bridge.preFee.ts +++ b/scripts/e2e/oft/bridge.preFee.ts @@ -30,7 +30,8 @@ import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); @@ -125,9 +126,17 @@ async function main(): Promise { const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + const bridgeData: BridgeData = { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -135,7 +144,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, USDT0_OFT_ADAPTER_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/oft/performExecution.postFee.ts b/scripts/e2e/oft/performExecution.postFee.ts index 67c0bbb..b8e668f 100644 --- a/scripts/e2e/oft/performExecution.postFee.ts +++ b/scripts/e2e/oft/performExecution.postFee.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = 0x01n | bridgeAmountPositionFlag(196); @@ -191,17 +191,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -217,7 +220,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -226,7 +229,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/performExecution.preFee.ts b/scripts/e2e/oft/performExecution.preFee.ts index ebb6ab8..cc16455 100644 --- a/scripts/e2e/oft/performExecution.preFee.ts +++ b/scripts/e2e/oft/performExecution.preFee.ts @@ -30,7 +30,8 @@ import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); @@ -125,9 +126,17 @@ async function main(): Promise { const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + const bridgeData: BridgeData = { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -135,7 +144,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, USDT0_OFT_ADAPTER_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/oft/performModularExecution.postFee.ts b/scripts/e2e/oft/performModularExecution.postFee.ts index 74e7e59..be9538b 100644 --- a/scripts/e2e/oft/performModularExecution.postFee.ts +++ b/scripts/e2e/oft/performModularExecution.postFee.ts @@ -35,12 +35,13 @@ import { USDT0_OFT_ADAPTER_POLYGON, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); @@ -143,8 +144,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -153,10 +152,18 @@ async function main() { ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, inputAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter, inputAmount, inputAmount); exec.call(ooRouter, swapData); exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ethers.MaxUint256, + ); const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); diff --git a/scripts/e2e/oft/performModularExecution.preFee.ts b/scripts/e2e/oft/performModularExecution.preFee.ts index 4f02cdc..297226e 100644 --- a/scripts/e2e/oft/performModularExecution.preFee.ts +++ b/scripts/e2e/oft/performModularExecution.preFee.ts @@ -30,12 +30,13 @@ import { USDT0_OFT_ADAPTER_POLYGON, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); @@ -102,7 +103,6 @@ async function main() { console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -112,7 +112,15 @@ async function main() { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDT0_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ethers.MaxUint256, + ); const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts index d865f0e..96f4944 100644 --- a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = 0x03n | bridgeAmountPositionFlag(196); @@ -191,17 +191,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -217,7 +220,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -226,7 +229,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts index fad5ebb..4980ec7 100644 --- a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = 0x01n | bridgeAmountPositionFlag(196); @@ -191,17 +191,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -217,7 +220,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -226,7 +229,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts index 789858f..8415223 100644 --- a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = 0x02n | bridgeAmountPositionFlag(196); @@ -190,17 +190,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + minAmountOut, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -216,7 +219,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -225,7 +228,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts index e8a60ae..5e5dcb7 100644 --- a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = bridgeAmountPositionFlag(196); @@ -190,17 +190,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + minAmountOut, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -216,7 +219,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -225,7 +228,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/relay/aave.bridge.preFee.ts b/scripts/e2e/relay/aave.bridge.preFee.ts index 2d90da3..73f4955 100644 --- a/scripts/e2e/relay/aave.bridge.preFee.ts +++ b/scripts/e2e/relay/aave.bridge.preFee.ts @@ -27,7 +27,8 @@ import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -74,11 +75,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); diff --git a/scripts/e2e/relay/aave.performExecution.preFee.ts b/scripts/e2e/relay/aave.performExecution.preFee.ts index 188ba82..70098a0 100644 --- a/scripts/e2e/relay/aave.performExecution.preFee.ts +++ b/scripts/e2e/relay/aave.performExecution.preFee.ts @@ -24,7 +24,8 @@ import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -71,11 +72,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); + + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); const execCalldata = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositData)); diff --git a/scripts/e2e/relay/aave.performModularExecution.preFee.ts b/scripts/e2e/relay/aave.performModularExecution.preFee.ts index 42076a0..dd9207e 100644 --- a/scripts/e2e/relay/aave.performModularExecution.preFee.ts +++ b/scripts/e2e/relay/aave.performModularExecution.preFee.ts @@ -26,13 +26,14 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -79,7 +80,6 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -87,7 +87,7 @@ async function main(): Promise { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); - exec.call(inputToken, encodeApprove(relaySpender, bridgeAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, inputToken, relaySpender, bridgeAmount, bridgeAmount); exec.call(depositTarget, depositData); const routerIface = new ethers.Interface(ROUTER_ABI); diff --git a/scripts/e2e/relay/usdc.bridge.preFee.ts b/scripts/e2e/relay/usdc.bridge.preFee.ts index 438d915..2fc5cbb 100644 --- a/scripts/e2e/relay/usdc.bridge.preFee.ts +++ b/scripts/e2e/relay/usdc.bridge.preFee.ts @@ -27,7 +27,8 @@ import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -74,11 +75,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); diff --git a/scripts/e2e/relay/usdc.performExecution.preFee.ts b/scripts/e2e/relay/usdc.performExecution.preFee.ts index baf4aff..e5ed43d 100644 --- a/scripts/e2e/relay/usdc.performExecution.preFee.ts +++ b/scripts/e2e/relay/usdc.performExecution.preFee.ts @@ -24,7 +24,8 @@ import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -71,11 +72,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); + + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; const routerIface = new ethers.Interface(ROUTER_ABI); const execCalldata = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositData)); diff --git a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts index d070ae0..9dc70e5 100644 --- a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts +++ b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts @@ -26,13 +26,14 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -79,7 +80,6 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -87,7 +87,7 @@ async function main(): Promise { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); - exec.call(inputToken, encodeApprove(relaySpender, bridgeAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, inputToken, relaySpender, bridgeAmount, bridgeAmount); exec.call(depositTarget, depositData); const routerIface = new ethers.Interface(ROUTER_ABI); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts index adaf4f6..81e4f8e 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -41,7 +41,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); const FLAGS = POST_FEE_FLAG | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -148,7 +149,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_ARB, + TOKENS.USDC_ARB, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress, amountLD); @@ -161,7 +169,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts index 2b8a109..aa0b93c 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts @@ -36,12 +36,13 @@ import { BASE_LZ_EID, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); @@ -147,7 +148,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); @@ -156,7 +156,7 @@ async function main() { ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_ARB, signerAddress, ROUTER_ARB, inputAmount])); - exec.call(TOKENS.USDC_ARB, encodeApprove(ooRouter, inputAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter, inputAmount, inputAmount); exec.call(ooRouter, swapData); exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(STARGATE_NATIVE_ARB, stargateData, bridgeValue); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts index ed16e8c..05d0c3f 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -203,7 +203,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -222,7 +229,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts index 7afc9c8..7521bcb 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts @@ -33,12 +33,13 @@ import { ARBITRUM_LZ_EID, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); @@ -143,7 +144,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); @@ -152,7 +152,7 @@ async function main() { ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_BASE, signerAddress, ROUTER_BASE, inputAmount])); - exec.call(TOKENS.USDC_BASE, encodeApprove(ooRouter, inputAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter, inputAmount, inputAmount); exec.call(ooRouter, swapData); exec.nativeCall(signerAddress, '0x', feeAmount); exec.nativeCall(STARGATE_NATIVE_BASE, stargateData, bridgeValue); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts index cd5fc38..87ca5eb 100644 --- a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts @@ -42,7 +42,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -182,7 +183,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); await ensureRouterNativeBalance(signer, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + estimatedBridgeAmount, + ); const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; @@ -206,7 +214,7 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, + { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer }, oftSendData, ), ); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts index b65902f..d46436b 100644 --- a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts @@ -36,12 +36,13 @@ import { STARGATE_AMOUNT_LD_OFFSET, } from '../config'; import { execViaAH } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf } from '../utils/erc20'; +import { encodeTransfer, encodeBalanceOf } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); @@ -176,7 +177,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); await ensureRouterNativeBalance(signer, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; @@ -186,7 +186,15 @@ async function main() { const exec = new ModularActionsBuilder(); exec.nativeCall(ooRouter, swapData, polOrEthToOo); exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + estimatedBridgeAmount, + ethers.MaxUint256, + ); const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); diff --git a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts index 80b62be..c2f6c2b 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts @@ -29,7 +29,8 @@ import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -123,9 +124,17 @@ async function main(): Promise { const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + STARGATE_USDC_POLYGON, + bridgeAmount, + ); + const bridgeData: BridgeData = { target: STARGATE_USDC_POLYGON, - approvalSpender: STARGATE_USDC_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -133,7 +142,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, STARGATE_USDC_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts index b4256dc..0009435 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -29,7 +29,8 @@ import { getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -123,9 +124,17 @@ async function main(): Promise { const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + STARGATE_USDC_POLYGON, + bridgeAmount, + ); + const bridgeData: BridgeData = { target: STARGATE_USDC_POLYGON, - approvalSpender: STARGATE_USDC_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -133,7 +142,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, STARGATE_USDC_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts index 1a5e457..ebb355a 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts @@ -31,12 +31,13 @@ import { STARGATE_AMOUNT_LD_OFFSET, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; import { ROUTER_ABI } from '../utils/routerAbi'; import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; import { ZERO_BYTES32 } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -101,7 +102,6 @@ async function main() { console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, STARGATE_USDC_POLYGON); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress); @@ -111,7 +111,15 @@ async function main() { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_POLYGON_CIRCLE, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(STARGATE_USDC_POLYGON, ethers.MaxUint256)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + STARGATE_USDC_POLYGON, + estimatedBridgeAmount, + ethers.MaxUint256, + ); const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); exec.nativeCall(STARGATE_USDC_POLYGON, stargateData, nativeFeeWithBuffer) .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts index 0b3a295..8be7ccb 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) const FLAGS = 0x03n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -202,7 +202,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -221,7 +228,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts index 10f84e2..ba1c483 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -203,7 +203,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -222,7 +229,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts index 00e5659..462ed18 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) const FLAGS = 0x02n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -202,7 +202,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + swapInput, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -221,7 +228,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts index 2324ea5..971437f 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee const FLAGS = BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); @@ -202,7 +202,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + swapInput, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -221,7 +228,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts index 570c08e..e1a0380 100644 --- a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) const FLAGS = 0x03n; @@ -176,11 +176,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -194,7 +196,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.postFee.returndata.ts b/scripts/e2e/swap/kyberswap.postFee.returndata.ts index 6da3af8..5deafac 100644 --- a/scripts/e2e/swap/kyberswap.postFee.returndata.ts +++ b/scripts/e2e/swap/kyberswap.postFee.returndata.ts @@ -38,8 +38,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | returndata (bit1=0 ⇒ no 0x02) const FLAGS = 0x01n; @@ -171,11 +171,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -189,7 +191,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts index 9f9312d..fbd0787 100644 --- a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts @@ -39,8 +39,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) const FLAGS = 0x02n; @@ -182,11 +182,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -200,7 +202,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.preFee.returndata.ts b/scripts/e2e/swap/kyberswap.preFee.returndata.ts index 59c9302..c9b84e4 100644 --- a/scripts/e2e/swap/kyberswap.preFee.returndata.ts +++ b/scripts/e2e/swap/kyberswap.preFee.returndata.ts @@ -40,8 +40,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | returndata (bit1=0 ⇒ no 0x02) const FLAGS = 0x00n; @@ -178,11 +178,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -196,7 +198,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.postFee.balanceOf.ts b/scripts/e2e/swap/swap.postFee.balanceOf.ts index 06e8377..70a71af 100644 --- a/scripts/e2e/swap/swap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.postFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) const FLAGS = 0x03n; @@ -118,11 +118,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -136,7 +138,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.postFee.returndata.ts b/scripts/e2e/swap/swap.postFee.returndata.ts index 9fa27fa..7fde5db 100644 --- a/scripts/e2e/swap/swap.postFee.returndata.ts +++ b/scripts/e2e/swap/swap.postFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | returndata (0x00) const FLAGS = 0x01n; @@ -113,11 +113,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -131,7 +133,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.preFee.balanceOf.ts b/scripts/e2e/swap/swap.preFee.balanceOf.ts index 4e4c5c0..d557b50 100644 --- a/scripts/e2e/swap/swap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.preFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) const FLAGS = 0x02n; @@ -118,11 +118,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -136,7 +138,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.preFee.returndata.ts b/scripts/e2e/swap/swap.preFee.returndata.ts index bd38078..094a548 100644 --- a/scripts/e2e/swap/swap.preFee.returndata.ts +++ b/scripts/e2e/swap/swap.preFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | returndata (0x00) const FLAGS = 0x00n; @@ -113,11 +113,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -131,7 +133,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/zerox.postFee.balanceOf.ts b/scripts/e2e/swap/zerox.postFee.balanceOf.ts index 0e1e7e0..7137d81 100644 --- a/scripts/e2e/swap/zerox.postFee.balanceOf.ts +++ b/scripts/e2e/swap/zerox.postFee.balanceOf.ts @@ -40,8 +40,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) const FLAGS = 0x03n; @@ -172,11 +172,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -190,7 +192,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/swap/zerox.postFee.returndata.ts b/scripts/e2e/swap/zerox.postFee.returndata.ts index 12253ef..8ec6e73 100644 --- a/scripts/e2e/swap/zerox.postFee.returndata.ts +++ b/scripts/e2e/swap/zerox.postFee.returndata.ts @@ -39,8 +39,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | returndata (no 0x02) const FLAGS = 0x01n; @@ -161,11 +161,13 @@ async function main() { const approvalSpender = ALLOWANCE_HOLDER; await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -179,7 +181,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/swap/zerox.preFee.balanceOf.ts b/scripts/e2e/swap/zerox.preFee.balanceOf.ts index 75b18a1..d3ec6f8 100644 --- a/scripts/e2e/swap/zerox.preFee.balanceOf.ts +++ b/scripts/e2e/swap/zerox.preFee.balanceOf.ts @@ -43,8 +43,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) const FLAGS = 0x02n; @@ -172,18 +172,19 @@ async function main() { // The 0x AllowanceHolder is the approval spender; swapTarget should equal ALLOWANCE_HOLDER const approvalSpender = ALLOWANCE_HOLDER; - await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance( signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -197,7 +198,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/swap/zerox.preFee.returndata.ts b/scripts/e2e/swap/zerox.preFee.returndata.ts index dfa9057..b955005 100644 --- a/scripts/e2e/swap/zerox.preFee.returndata.ts +++ b/scripts/e2e/swap/zerox.preFee.returndata.ts @@ -41,8 +41,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | returndata (no 0x02) const FLAGS = 0x00n; @@ -167,15 +167,15 @@ async function main() { console.log(` 0x target: ${swapTarget}`); console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); - const approvalSpender = ALLOWANCE_HOLDER; - await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -189,7 +189,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/utils/reproducibility.ts b/scripts/e2e/utils/reproducibility.ts index 6e34f9f..62e49da 100644 --- a/scripts/e2e/utils/reproducibility.ts +++ b/scripts/e2e/utils/reproducibility.ts @@ -7,10 +7,9 @@ * Before each test leg these ensure: * 1. The router holds ≥ 20 wei of every token whose balance slot will be written. * - * Router→spender ERC-20 approvals are NOT pre-seeded here. `BungeeOpenRouter` - * sets max allowance inside `swap`, `bridge`, and `swapAndBridge` when needed. - * Modular `performActions` legs may still include inline `approve` actions in the - * same transaction when testing raw modular flows. + * Router→spender approvals are handled per-script via `routerAllowance.ts` (check allowance, + * then set `approvalSpender` or modular `approve` only when insufficient). The contract also + * approves inside `swap` / `bridge` / `swapAndBridge` when `approvalSpender` is non-zero. * * Seeding balance slots to non-zero means subsequent SSTORE writes cost ~2 900 gas * (non-zero → non-zero) rather than ~20 000 gas (zero → non-zero), giving @@ -65,18 +64,3 @@ export async function ensureRouterNativeBalance( const tx = await signer.sendTransaction({ to: openRouter, value: SEED_WEI }); await tx.wait(); } - -/** - * No-op: router→spender approvals are handled by the contract on `swap` / - * `bridge` / `swapAndBridge`. Kept so existing e2e scripts do not need rewrites. - * - * @deprecated Pre-approval via a separate `performActions` tx is intentionally disabled. - */ -export async function ensureRouterApproval( - _signer: ethers.Wallet, - _openRouterAddress: string, - _token: string, - _spender: string, -): Promise { - // Intentionally empty — see module header. -} diff --git a/scripts/e2e/utils/routerAllowance.ts b/scripts/e2e/utils/routerAllowance.ts new file mode 100644 index 0000000..3129eeb --- /dev/null +++ b/scripts/e2e/utils/routerAllowance.ts @@ -0,0 +1,110 @@ +/** + * Router ERC-20 allowance helpers for e2e scripts. + * + * `BungeeOpenRouter` only calls `approve` when `approvalSpender != 0` and + * `requiredAmount > allowance(router, spender)`. Scripts mirror that: check on-chain + * allowance first, omit modular approve actions when sufficient, and pass + * `ZERO_ADDRESS` as `approvalSpender` on `swap` / `bridge` / `swapAndBridge` when not needed. + */ +import { ethers } from 'ethers'; + +import { NATIVE_TOKEN_ADDRESS } from '../config'; +import { ZERO_ADDRESS } from './contractTypes'; +import { encodeApprove, getErc20Contract } from './erc20'; + +export interface ModularActionsExec { + call(target: string, data: string): unknown; +} + +/** + * Reads `token.allowance(router, spender)`. + */ +export async function readRouterAllowance( + provider: ethers.Provider, + routerAddress: string, + tokenAddress: string, + spenderAddress: string, +): Promise { + const router = ethers.getAddress(routerAddress); + const token = ethers.getAddress(tokenAddress); + const spender = ethers.getAddress(spenderAddress); + const erc20 = getErc20Contract(token, provider); + const allowanceRaw = await erc20.allowance(router, spender); + return typeof allowanceRaw === 'bigint' ? allowanceRaw : BigInt(allowanceRaw.toString()); +} + +/** + * Matches contract logic: approval is skipped when `allowance >= requiredAmount`. + */ +export function routerAllowanceSufficient(allowance: bigint, requiredAmount: bigint): boolean { + return allowance >= requiredAmount; +} + +function isNativeToken(tokenAddress: string): boolean { + return ethers.getAddress(tokenAddress) === ethers.getAddress(NATIVE_TOKEN_ADDRESS); +} + +function isZeroSpender(spenderAddress: string): boolean { + return ethers.getAddress(spenderAddress) === ethers.getAddress(ZERO_ADDRESS); +} + +/** + * Returns `spender` for `SwapData` / `BridgeData` when the router must approve, else `ZERO_ADDRESS`. + */ +export async function resolveApprovalSpender( + provider: ethers.Provider, + routerAddress: string, + tokenAddress: string, + spenderAddress: string, + requiredAmount: bigint, +): Promise { + if (isNativeToken(tokenAddress) || isZeroSpender(spenderAddress)) { + return ZERO_ADDRESS; + } + + const allowance = await readRouterAllowance(provider, routerAddress, tokenAddress, spenderAddress); + if (routerAllowanceSufficient(allowance, requiredAmount)) { + console.log( + ` [allowance] sufficient: token=${tokenAddress} spender=${spenderAddress} allowance=${allowance} required=${requiredAmount} → approvalSpender=0`, + ); + return ZERO_ADDRESS; + } + + console.log( + ` [allowance] insufficient: token=${tokenAddress} spender=${spenderAddress} allowance=${allowance} required=${requiredAmount} → approvalSpender set`, + ); + return ethers.getAddress(spenderAddress); +} + +/** + * Appends a modular `approve` action only when router allowance is below `requiredAmount`. + * + * @returns true when an approve action was added. + */ +export async function modularApproveIfNeeded( + exec: ModularActionsExec, + provider: ethers.Provider, + routerAddress: string, + tokenAddress: string, + spenderAddress: string, + requiredAmount: bigint, + approveAmount: bigint = ethers.MaxUint256, +): Promise { + if (isNativeToken(tokenAddress) || isZeroSpender(spenderAddress)) { + return false; + } + + const allowance = await readRouterAllowance(provider, routerAddress, tokenAddress, spenderAddress); + if (routerAllowanceSufficient(allowance, requiredAmount)) { + console.log( + ` [allowance] skipping modular approve: token=${tokenAddress} spender=${spenderAddress} allowance=${allowance} required=${requiredAmount}`, + ); + return false; + } + + console.log( + ` [allowance] modular approve: token=${tokenAddress} spender=${spenderAddress} amount=${approveAmount}`, + ); + exec.call(tokenAddress, encodeApprove(spenderAddress, approveAmount)); + return true; +} From b51be8969606ef7ede53b421152c8bac99785dae Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 23:31:02 +0530 Subject: [PATCH 60/69] ci: remove fmt --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b79c8d4..3b4b0b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,9 +28,6 @@ jobs: - name: Show Forge version run: forge --version - - name: Run Forge fmt - run: forge fmt --check - - name: Run Forge build run: forge build --sizes From d05a66b7dd644d0ef9bc4f9f0f6d0ff1b1b49a72 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Tue, 19 May 2026 23:32:10 +0530 Subject: [PATCH 61/69] ci: run on push to main only --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b4b0b5..7422605 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,8 @@ permissions: {} on: push: + branches: + - main pull_request: workflow_dispatch: From d313ed2b8d97e74431757dde449482dc8e3b0faf Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 20 May 2026 00:20:38 +0530 Subject: [PATCH 62/69] refactor: rename contract name --- .env.example | 1 - AGENTS.md | 2 +- OPENROUTER.md | 32 +++++++++---------- OPENROUTER_ASSUMPTIONS.md | 14 ++++---- OPENROUTER_CONTEXT.md | 12 +++---- package.json | 4 +-- ...ungeeOpenRouter.ts => deployOpenRouter.ts} | 12 +++---- scripts/e2e/approveViaModular.ts | 2 +- scripts/e2e/config.ts | 2 +- .../e2e/misc/routerUsdc.withdraw.modular.ts | 2 +- scripts/e2e/utils/contractTypes.ts | 2 +- scripts/e2e/utils/routerAbi.ts | 2 +- scripts/e2e/utils/routerAllowance.ts | 2 +- src/{BungeeOpenRouter.sol => OpenRouter.sol} | 4 +-- ....sol => OpenRouterV2UncheckedBridge.t.sol} | 6 ++-- ....t.sol => OpenRouterV2UncheckedSwap.t.sol} | 6 ++-- ... OpenRouterV2UncheckedSwapAndBridge.t.sol} | 6 ++-- ....sol => OpenRouterV2UncheckedTestBase.sol} | 4 +-- test/poc/OneInchCctpOpenRouterPoC.t.sol | 2 +- test/poc/OpenOceanAcrossOpenRouterPoC.t.sol | 2 +- ...OpenOceanStargateNativeOpenRouterPoC.t.sol | 2 +- 21 files changed, 60 insertions(+), 61 deletions(-) rename scripts/deploy/{deployBungeeOpenRouter.ts => deployOpenRouter.ts} (74%) rename src/{BungeeOpenRouter.sol => OpenRouter.sol} (99%) rename test/combined/{BungeeOpenRouterV2UncheckedBridge.t.sol => OpenRouterV2UncheckedBridge.t.sol} (95%) rename test/combined/{BungeeOpenRouterV2UncheckedSwap.t.sol => OpenRouterV2UncheckedSwap.t.sol} (97%) rename test/combined/{BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol => OpenRouterV2UncheckedSwapAndBridge.t.sol} (96%) rename test/combined/{BungeeOpenRouterV2UncheckedTestBase.sol => OpenRouterV2UncheckedTestBase.sol} (98%) diff --git a/.env.example b/.env.example index 528f41e..74fa06b 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,6 @@ PRIVATE_KEY= # Constructor arguments OWNER_ADDRESS= -OPEN_ROUTER_SIGNER_ADDRESS= # only needed for BungeeOpenRouterV2 (not Unchecked) # External API keys RELAY_API_KEY= # optional, relay.link x-api-key header diff --git a/AGENTS.md b/AGENTS.md index f714adb..7e55c30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,4 +3,4 @@ For OpenRouter contract work read files which are relevant for the task. general context - `OPENROUTER_CONTEXT.md` assumptions - `OPENROUTER_ASSUMPTIONS.md` first. -Main ship target is `src/combined/BungeeOpenRouterV2Unchecked.sol`. If its ABI changes, update the backend encoders in `bungee-backend/src/modules/dex/utils.ts` and `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts`. +Main ship target is `src/combined/OpenRouterV2Unchecked.sol`. If its ABI changes, update the backend encoders in `bungee-backend/src/modules/dex/utils.ts` and `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts`. diff --git a/OPENROUTER.md b/OPENROUTER.md index ced6eec..a705e80 100644 --- a/OPENROUTER.md +++ b/OPENROUTER.md @@ -1,4 +1,4 @@ -# BungeeOpenRouter — Contract Variants +# OpenRouter — Contract Variants > **Monolithic** — non-generic; purpose-built with fees, swap, bridge functionality @@ -24,14 +24,14 @@ src/ interfaces/IAllowanceHolder.sol allowance/AllowanceHolderContext.sol monolithic/ - BungeeOpenRouter.sol - BungeeOpenRouterAH.sol + OpenRouter.sol + OpenRouterAH.sol modular/ - BungeeOpenRouterModular.sol - BungeeOpenRouterModularAH.sol + OpenRouterModular.sol + OpenRouterModularAH.sol minimal/ - BungeeOpenRouterMinimal.sol - BungeeOpenRouterMinimalAH.sol + OpenRouterMinimal.sol + OpenRouterMinimalAH.sol ``` Each variant subdirectory holds the ERC20-facing contract and its AllowanceHolder sibling; imports reach into `../common/`. @@ -74,9 +74,9 @@ The contract has no reentrancy guard, matching `Solver` and `StakedRouterReceive --- -## v1 — BungeeOpenRouter (monolithic) +## v1 — OpenRouter (monolithic) -**File:** [`src/monolithic/BungeeOpenRouter.sol`](src/monolithic/BungeeOpenRouter.sol). AllowanceHolder variant: [`src/monolithic/BungeeOpenRouterAH.sol`](src/monolithic/BungeeOpenRouterAH.sol). +**File:** [`src/monolithic/OpenRouter.sol`](src/monolithic/OpenRouter.sol). AllowanceHolder variant: [`src/monolithic/OpenRouterAH.sol`](src/monolithic/OpenRouterAH.sol). This version encodes the full execution pipeline directly in the contract. The steps are explicit, ordered, and named. The signed payload is a single `Execution` struct: @@ -130,13 +130,13 @@ assembly ("memory-safe") { **When to use this.** Routes where the shape of the flow is always the same: pull → optional pre-fee → optional swap → optional post-fee → bridge. The contract knows the meaning of every field and enforces sensible preconditions (e.g. `finalAmount` cannot underflow below a fee). Adding a step that does not fit this shape — like a second bridge call, a pre-swap approval to a different address, or an intermediate hop — is not possible without deploying a new version of the contract. -**AllowanceHolder variant (`BungeeOpenRouterAH`).** Instead of pulling with ERC20 `transferFrom` from the user to the router, the pull step calls 0x `AllowanceHolder.transferFrom` so funds move under that contract’s transient allowance (user approves AllowanceHolder, user calls `AllowanceHolder.exec` with `target = this router` and calldata invoking `performExecution`). The AH entry decodes `_msgSender()` as the original user appended by AllowanceHolder; `_pullFromUser` requires `_msgSender() == user`, so only the signer-named user matches the ephemeral allowance binding. Like Settler + AH patterns, `AllowanceHolderContext` exposes a harmless `balanceOf` on the router so AllowanceHolder’s confused-deputy probe succeeds; the rest of the pipeline is unchanged. +**AllowanceHolder variant (`OpenRouterAH`).** Instead of pulling with ERC20 `transferFrom` from the user to the router, the pull step calls 0x `AllowanceHolder.transferFrom` so funds move under that contract’s transient allowance (user approves AllowanceHolder, user calls `AllowanceHolder.exec` with `target = this router` and calldata invoking `performExecution`). The AH entry decodes `_msgSender()` as the original user appended by AllowanceHolder; `_pullFromUser` requires `_msgSender() == user`, so only the signer-named user matches the ephemeral allowance binding. Like Settler + AH patterns, `AllowanceHolderContext` exposes a harmless `balanceOf` on the router so AllowanceHolder’s confused-deputy probe succeeds; the rest of the pipeline is unchanged. --- -## v2 — BungeeOpenRouterModular (generic actions + returndata splicing) +## v2 — OpenRouterModular (generic actions + returndata splicing) -**File:** [`src/modular/BungeeOpenRouterModular.sol`](src/modular/BungeeOpenRouterModular.sol). AllowanceHolder variant: [`src/modular/BungeeOpenRouterModularAH.sol`](src/modular/BungeeOpenRouterModularAH.sol). +**File:** [`src/modular/OpenRouterModular.sol`](src/modular/OpenRouterModular.sol). AllowanceHolder variant: [`src/modular/OpenRouterModularAH.sol`](src/modular/OpenRouterModularAH.sol). This version removes all domain-specific knowledge from the contract. The only signed payload is a list of `Action`s: @@ -191,13 +191,13 @@ Both source and destination offsets are bounds-checked before the copy; zero-len **When to use this.** Any route where the exact amount flowing between steps is not known until runtime and must be piped into the next step's calldata. The canonical motivating case is an integration like Across, where two separate fields in the bridge calldata both need to reflect the swap output amount. With `GenericStakedRoute` you can only patch one offset; with this contract you declare as many splices as needed, each targeting a different offset. -**AllowanceHolder variant (`BungeeOpenRouterModularAH`).** The action loop is identical after verification: no built-in pull. You choose how to compose an AllowanceHolder `transferFrom` (or delegatecall shim) as one or more ordinary `CALL` actions signed with everything else; `performExecutionAH` wraps that by binding the signature to `(chainId, this, signedUser, exec)` instead of omitting `signedUser`. It asserts `_msgSender() == signedUser` so nobody can burn another user’s nonce by submitting their payload inside a stranger’s `AH.exec`; real fund safety still comes from AllowanceHolder’s operator/owner/token scoping; `AllowanceHolderContext` only supplies the dummy `balanceOf` for AH’s probing. +**AllowanceHolder variant (`OpenRouterModularAH`).** The action loop is identical after verification: no built-in pull. You choose how to compose an AllowanceHolder `transferFrom` (or delegatecall shim) as one or more ordinary `CALL` actions signed with everything else; `performExecutionAH` wraps that by binding the signature to `(chainId, this, signedUser, exec)` instead of omitting `signedUser`. It asserts `_msgSender() == signedUser` so nobody can burn another user’s nonce by submitting their payload inside a stranger’s `AH.exec`; real fund safety still comes from AllowanceHolder’s operator/owner/token scoping; `AllowanceHolderContext` only supplies the dummy `balanceOf` for AH’s probing. --- -## v3 — BungeeOpenRouterMinimal (generic actions, no splicing) +## v3 — OpenRouterMinimal (generic actions, no splicing) -**File:** [`src/minimal/BungeeOpenRouterMinimal.sol`](src/minimal/BungeeOpenRouterMinimal.sol). AllowanceHolder variant: [`src/minimal/BungeeOpenRouterMinimalAH.sol`](src/minimal/BungeeOpenRouterMinimalAH.sol). +**File:** [`src/minimal/OpenRouterMinimal.sol`](src/minimal/OpenRouterMinimal.sol). AllowanceHolder variant: [`src/minimal/OpenRouterMinimalAH.sol`](src/minimal/OpenRouterMinimalAH.sol). This version is the stripped-down sibling of v2. The `Action` struct has no `splices` field: @@ -224,7 +224,7 @@ This is exactly how `BaseRouterSingleOutput` works: it measures the swap output **When to use this.** Routes where every action is self-contained — the called contracts know what token to look at, query their own balance, and use that as their amount. This covers most `GenericStakedRoute` flows today, since those contracts already contain the offset-patching and balance-reading logic. v3 is the right choice when you do not need cross-action data passing at the router layer, and you want the smallest possible trusted surface in the router contract itself. -**AllowanceHolder variant (`BungeeOpenRouterMinimalAH`).** Same idea as the modular AH: use `performExecutionAH` plus `AllowanceHolderContext`’s `balanceOf`; sign over `signedUser` and require `_msgSender() == signedUser` for nonce-binding; compose the AH pull as ordinary actions in `exec.actions`. +**AllowanceHolder variant (`OpenRouterMinimalAH`).** Same idea as the modular AH: use `performExecutionAH` plus `AllowanceHolderContext`’s `balanceOf`; sign over `signedUser` and require `_msgSender() == signedUser` for nonce-binding; compose the AH pull as ordinary actions in `exec.actions`. --- diff --git a/OPENROUTER_ASSUMPTIONS.md b/OPENROUTER_ASSUMPTIONS.md index 7ba02a6..98bff2a 100644 --- a/OPENROUTER_ASSUMPTIONS.md +++ b/OPENROUTER_ASSUMPTIONS.md @@ -2,20 +2,20 @@ Last reviewed: 2026-05-19. -Scope: `src/combined/BungeeOpenRouterV2Unchecked.sol`. +Scope: `src/combined/OpenRouterV2Unchecked.sol`. This document captures the assumptions that make the unchecked OpenRouter safe to operate. Many of these are business and integration assumptions, not guarantees enforced by the contract. ## Source Of Truth -`BungeeOpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone. +`OpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone. Current checked-in public surface: - `swap(...)` - `swapAndBridge(...)` - `bridge(...)` -- `performModularExecution(...)` +- `performActions()(...)` - `rescueFunds(...)` `OPENROUTER_CONTEXT.md` and `scripts/e2e/utils/routerAbi.ts` may mention `performExecution(...)`; verify against the Solidity file before relying on that ABI. @@ -34,7 +34,7 @@ Use this distinction when reviewing any route or integration: The router may temporarily hold funds during one transaction, but it should not end routes with meaningful token or native balances. -Failure mode: `performModularExecution` lets any caller make the router call arbitrary contracts. If the router holds ERC20s, native ETH, bridged refunds, swap dust, rebates, or protocol refunds, a public caller can move or approve those assets through modular actions before owner rescue. +Failure mode: `performActions()` lets any caller make the router call arbitrary contracts. If the router holds ERC20s, native ETH, bridged refunds, swap dust, rebates, or protocol refunds, a public caller can move or approve those assets through modular actions before owner rescue. Operational requirements: @@ -47,7 +47,7 @@ Operational requirements: Users must not give persistent ERC20, Permit2, ERC721, ERC1155, or protocol-specific approvals directly to the router. -Failure mode: if a user directly approves the router, any caller can use `performModularExecution` to make the router call `transferFrom`, `approve`, or equivalent privileged token functions against that user allowance. +Failure mode: if a user directly approves the router, any caller can use `performActions()` to make the router call `transferFrom`, `approve`, or equivalent privileged token functions against that user allowance. Operational requirements: @@ -194,7 +194,7 @@ Failure modes: ## Modular Execution Assumptions -`performModularExecution` is the broadest surface. It makes the router a public generic call executor. +`performActions()` is the broadest surface. It makes the router a public generic call executor. Assumptions: @@ -245,4 +245,4 @@ Before enabling a route or integration, confirm: - Excess native value and bridge refunds do not end up on the router. - Monitoring exists for router balances, direct allowances to router, and unexpected downstream roles. -If any critical business assumption is false, do not rely on `BungeeOpenRouterV2Unchecked` as-is. Add access control, use a signed variant, or remove the downstream privilege/funds/allowance that makes the public call surface dangerous. +If any critical business assumption is false, do not rely on `OpenRouterV2Unchecked` as-is. Add access control, use a signed variant, or remove the downstream privilege/funds/allowance that makes the public call surface dangerous. diff --git a/OPENROUTER_CONTEXT.md b/OPENROUTER_CONTEXT.md index 94a4248..2736eb4 100644 --- a/OPENROUTER_CONTEXT.md +++ b/OPENROUTER_CONTEXT.md @@ -4,13 +4,13 @@ Last researched: 2026-05-18. Main ship target: -- `src/combined/BungeeOpenRouterV2Unchecked.sol` +- `src/combined/OpenRouterV2Unchecked.sol` -Use `src/combined/BungeeOpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI. +Use `src/combined/OpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI. ## V2Unchecked Surface -`BungeeOpenRouterV2Unchecked` removes backend signature verification, nonce, and deadline fields. Fund safety for ERC20 inputs depends on 0x AllowanceHolder transient approvals plus `_msgSender() == input.user` in `_pullFromUser`. +`OpenRouterV2Unchecked` removes backend signature verification, nonce, and deadline fields. Fund safety for ERC20 inputs depends on 0x AllowanceHolder transient approvals plus `_msgSender() == input.user` in `_pullFromUser`. External entrypoints: @@ -29,12 +29,12 @@ External entrypoints: - `bridge(bytes32 requestHash, InputData input, FeeData fee, BridgeData bridgeData, bytes bridgeCallData)` - Direct bridge, no swap. - No runtime splice; bridge amount must already be encoded in `bridgeCallData`. -- `performModularExecution(bytes32 requestHash, Action[] actions)` +- `performActions()(bytes32 requestHash, Action[] actions)` - Generic action loop with packed action metadata and packed splices. ## Flags -Flag constants in `BungeeOpenRouterV2Unchecked.sol`: +Flag constants in `OpenRouterV2Unchecked.sol`: - `0x01` - post-swap fee for `swap` and `swapAndBridge`; clear means pre-fee from input. Ignored by `performExecution`. - `0x02` - measure swap output by `balanceOf` delta; clear means decode return word at `SwapData.returnDataWordOffset`. @@ -105,4 +105,4 @@ If the Solidity ABI changes, update those hard-coded ABI strings first. Direct D - `bridge()` cannot splice runtime amounts. Use `swapAndBridge()` when bridge calldata needs the live swap output. - `swapAndBridge()` uses balance-delta output measurement in backend builders today. - `performExecution` and `swapAndBridge` share helpers but have different fee semantics. -- Production use of `BungeeOpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks. +- Production use of `OpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks. diff --git a/package.json b/package.json index e3000a6..b1a1749 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "compile": "hardhat compile", - "deploy": "hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network", - "deploy:v2": "hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network", + "deploy": "hardhat run scripts/deploy/deployOpenRouter.ts --network", + "deploy:v2": "hardhat run scripts/deploy/deployOpenRouterV2.ts --network", "typechain": "hardhat typechain", "slither": "bash scripts/docker-slither.sh" }, diff --git a/scripts/deploy/deployBungeeOpenRouter.ts b/scripts/deploy/deployOpenRouter.ts similarity index 74% rename from scripts/deploy/deployBungeeOpenRouter.ts rename to scripts/deploy/deployOpenRouter.ts index d5db918..6afcb60 100644 --- a/scripts/deploy/deployBungeeOpenRouter.ts +++ b/scripts/deploy/deployOpenRouter.ts @@ -1,8 +1,8 @@ /** - * Deployment script for BungeeOpenRouter. + * Deployment script for OpenRouter. * * Usage: - * npx hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network + * npx hardhat run scripts/deploy/deployOpenRouter.ts --network * * Required env vars: * DEPLOYER_PRIVATE_KEY — deployer wallet private key @@ -23,15 +23,15 @@ async function main() { console.log('Network: ', networkName); console.log(''); - console.log('Deploying BungeeOpenRouter...'); - const factory = await ethers.getContractFactory('BungeeOpenRouter'); + console.log('Deploying OpenRouter...'); + const factory = await ethers.getContractFactory('OpenRouter'); const router = await factory.deploy(owner); await router.waitForDeployment(); const routerAddress = await router.getAddress(); - console.log('BungeeOpenRouter deployed to:', routerAddress); + console.log('OpenRouter deployed to:', routerAddress); console.log('\n=== Deployment Summary ==='); - console.log(`BungeeOpenRouter: ${routerAddress}`); + console.log(`OpenRouter: ${routerAddress}`); const chainId = (await ethers.provider.getNetwork()).chainId; if (chainId !== 31337n) { diff --git a/scripts/e2e/approveViaModular.ts b/scripts/e2e/approveViaModular.ts index 8a62bdb..891a7e2 100644 --- a/scripts/e2e/approveViaModular.ts +++ b/scripts/e2e/approveViaModular.ts @@ -2,7 +2,7 @@ * Script — Call ERC-20 approve(spender, amount) through the router using * `performActions(Action[])`. * - * DISABLED by default: `BungeeOpenRouter` now sets max allowance inside + * DISABLED by default: `OpenRouter` now sets max allowance inside * `swap`, `bridge`, and `swapAndBridge`. Use those entrypoints instead of a * standalone approval tx. Set `E2E_ENABLE_MODULAR_PRE_APPROVE=1` only if you * need this legacy helper for manual modular debugging. diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 2a25808..5aca8d4 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -29,7 +29,7 @@ export const BLOCK_EXPLORER_TX_PREFIX: Record = { export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; /** - * Deployed `BungeeOpenRouterV2Unchecked` — one address per chain used by e2e scripts. + * Deployed `OpenRouterV2Unchecked` — one address per chain used by e2e scripts. * Override per chain with env `ROUTER_CHAIN_` (e.g. ROUTER_CHAIN_1 for Ethereum). * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. */ diff --git a/scripts/e2e/misc/routerUsdc.withdraw.modular.ts b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts index 4df585d..6ea546f 100644 --- a/scripts/e2e/misc/routerUsdc.withdraw.modular.ts +++ b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts @@ -1,5 +1,5 @@ /** - * Polygon: sweep USDC from `BungeeOpenRouter` to the tx sender using + * Polygon: sweep USDC from `OpenRouter` to the tx sender using * `performActions` only — no AllowanceHolder, no pull step. * * Actions: diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index a55c4cb..9bf07c2 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -1,5 +1,5 @@ /** - * TypeScript interfaces mirroring BungeeOpenRouter Solidity structs. + * TypeScript interfaces mirroring OpenRouter Solidity structs. * Field names and order must match the compiler ABI encoding. */ diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index 03dc419..7e2e702 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -1,5 +1,5 @@ /** - * ABI fragments for BungeeOpenRouter entrypoints used by e2e scripts. + * ABI fragments for OpenRouter entrypoints used by e2e scripts. * Struct field order must match the Solidity definitions. */ export const ROUTER_ABI = [ diff --git a/scripts/e2e/utils/routerAllowance.ts b/scripts/e2e/utils/routerAllowance.ts index 3129eeb..15c382b 100644 --- a/scripts/e2e/utils/routerAllowance.ts +++ b/scripts/e2e/utils/routerAllowance.ts @@ -1,7 +1,7 @@ /** * Router ERC-20 allowance helpers for e2e scripts. * - * `BungeeOpenRouter` only calls `approve` when `approvalSpender != 0` and + * `OpenRouter` only calls `approve` when `approvalSpender != 0` and * `requiredAmount > allowance(router, spender)`. Scripts mirror that: check on-chain * allowance first, omit modular approve actions when sufficient, and pass * `ZERO_ADDRESS` as `approvalSpender` on `swap` / `bridge` / `swapAndBridge` when not needed. diff --git a/src/BungeeOpenRouter.sol b/src/OpenRouter.sol similarity index 99% rename from src/BungeeOpenRouter.sol rename to src/OpenRouter.sol index 685a81b..858c00d 100644 --- a/src/BungeeOpenRouter.sol +++ b/src/OpenRouter.sol @@ -12,12 +12,12 @@ import {CurrencyLib} from "./common/lib/CurrencyLib.sol"; import {RescueFundsLib} from "./common/lib/RescueFundsLib.sol"; import {RESCUE_ROLE} from "./common/AccessRoles.sol"; -/// @title BungeeOpenRouter +/// @title OpenRouter /// @notice Pull → optional fee → swap/bridge execution without backend signature verification. /// Fund safety rests on AllowanceHolder's transient allowance scoping (operator + owner + token): /// only the user whose address was passed to `AllowanceHolder.exec` can authorise a pull of /// their own funds. The `_msgSender() == user` check in `_pullFromUser` enforces this. -contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { +contract OpenRouter is AccessControl, AllowanceHolderContext { using SafeTransferLib for address; // ========================================================================= diff --git a/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol b/test/combined/OpenRouterV2UncheckedBridge.t.sol similarity index 95% rename from test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol rename to test/combined/OpenRouterV2UncheckedBridge.t.sol index 50635d0..e6c5f81 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol +++ b/test/combined/OpenRouterV2UncheckedBridge.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; -import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; -contract BungeeOpenRouterV2UncheckedBridgeTest is BungeeOpenRouterV2UncheckedTestBase { +contract OpenRouterV2UncheckedBridgeTest is OpenRouterV2UncheckedTestBase { function test_bridge_erc20() public { _deal(address(inputToken), USER, INPUT_AMOUNT); _approveInputToken(INPUT_AMOUNT); diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol b/test/combined/OpenRouterV2UncheckedSwap.t.sol similarity index 97% rename from test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol rename to test/combined/OpenRouterV2UncheckedSwap.t.sol index bf60bb4..b36aca1 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol +++ b/test/combined/OpenRouterV2UncheckedSwap.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; -import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; -contract BungeeOpenRouterV2UncheckedSwapTest is BungeeOpenRouterV2UncheckedTestBase { +contract OpenRouterV2UncheckedSwapTest is OpenRouterV2UncheckedTestBase { function test_swapWithReturnData() public { _deal(address(inputToken), USER, INPUT_AMOUNT); _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol b/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol similarity index 96% rename from test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol rename to test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol index f9376b8..3eddd22 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol +++ b/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; -import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; -contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2UncheckedTestBase { +contract OpenRouterV2UncheckedSwapAndBridgeTest is OpenRouterV2UncheckedTestBase { enum FeeMode { None, Pre, diff --git a/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol b/test/combined/OpenRouterV2UncheckedTestBase.sol similarity index 98% rename from test/combined/BungeeOpenRouterV2UncheckedTestBase.sol rename to test/combined/OpenRouterV2UncheckedTestBase.sol index 1704097..1b3fda1 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol +++ b/test/combined/OpenRouterV2UncheckedTestBase.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; -abstract contract BungeeOpenRouterV2UncheckedTestBase is Test { +abstract contract OpenRouterV2UncheckedTestBase is Test { uint256 internal constant FEE_FLAG_BIT_MASK = 0x01; uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; uint256 internal constant BRIDGE_VALUE_FLAG_BIT_MASK = 0x04; diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 8065379..d5f939e 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; interface ITokenMessengerV2 { diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 691ae10..57522fd 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {AcrossERC20AmountManipulator} from "../../src/manipulators/AcrossERC20AmountManipulator.sol"; interface ISpokePool { diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index 74b9545..f9ae56a 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {MathManipulator} from "../../src/manipulators/MathManipulator.sol"; interface IOpenOceanExchangeV2 { From 02494d13b702312b309b0127a509f086976a49de Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 20 May 2026 00:25:11 +0530 Subject: [PATCH 63/69] docs: update --- OPENROUTER.md | 332 ++++++++++++++++++++++---------------------------- 1 file changed, 149 insertions(+), 183 deletions(-) diff --git a/OPENROUTER.md b/OPENROUTER.md index a705e80..17b94a4 100644 --- a/OPENROUTER.md +++ b/OPENROUTER.md @@ -1,272 +1,238 @@ -# OpenRouter — Contract Variants +# OpenRouter -> **Monolithic** — non-generic; purpose-built with fees, swap, bridge functionality +**Contract:** [`src/OpenRouter.sol`](src/OpenRouter.sol) -> **Modular** — generic; supports arbitrary actions; uses returndata from previous calls and modifies parts of next calldata +OpenRouter is a single on-chain executor that combines two earlier designs: -> **Minimal** — generic; supports arbitrary actions; but no calldata modification; each subsequent action destination contract can read state eg. balanceOf() and uses them as needed; +1. **Structured (monolithic) routes** — fixed pull → fee → swap → bridge semantics, exposed as separate entrypoints (`swap`, `swapAndBridge`, `bridge`) instead of one giant `Execution` struct and `performExecution`. +2. **Generic (modular) routes** — an ordered `performActions` loop with returndata splicing between steps, for flows that do not fit the structured pipeline. -All versions uses signature verification. +There is **no backend signature verification**, **no nonce**, and **no deadline** on this contract. ERC-20 fund safety for structured pulls relies on [0x AllowanceHolder](https://github.com/0xProject/0x-settler) transient allowances plus `_msgSender() == input.user` in `_pullFromUser`. Native input uses `msg.value` on the outer call. -Three versions of the OpenRouter contract exist, each making a different trade-off between rigidity and generality. All three share the same authentication model; they differ only in how the execution steps are expressed and how outputs flow between steps. +--- -**Source layout** (under `src/`): +## Source layout ```text src/ - Counter.sol # scaffold only - common/ # shared by every variant + AH offshoots - OpenRouterAuthBase.sol - lib/AuthenticationLib.sol + OpenRouter.sol # ship target + common/ + allowance/AllowanceHolderContext.sol + interfaces/IAllowanceHolder.sol lib/BytesSpliceLib.sol lib/CurrencyLib.sol - utils/Ownable.sol - interfaces/IAllowanceHolder.sol - allowance/AllowanceHolderContext.sol - monolithic/ - OpenRouter.sol - OpenRouterAH.sol - modular/ - OpenRouterModular.sol - OpenRouterModularAH.sol - minimal/ - OpenRouterMinimal.sol - OpenRouterMinimalAH.sol + lib/RescueFundsLib.sol + utils/AccessControl.sol + manipulators/ # optional off-router helpers for PoCs (Across, math) ``` -Each variant subdirectory holds the ERC20-facing contract and its AllowanceHolder sibling; imports reach into `../common/`. - --- -## What is shared across all three +## How users call the router -Every version inherits `OpenRouterAuthBase` from [`src/common/OpenRouterAuthBase.sol`](src/common/OpenRouterAuthBase.sol). The only things hard-wired in the contract are: +ERC-20 inputs must be submitted through **AllowanceHolder**, not by calling OpenRouter directly: -- **A single trusted signer** (`OPEN_ROUTER_SIGNER`), rotatable by the owner via two-step `Ownable`. This is the backend solver/orchestration service address. -- **Per-nonce replay protection.** A `nonceUsed` mapping is written with an assembly `sstore` the moment a valid signature is verified. Any attempt to resubmit the same nonce reverts with `InvalidNonce()` before touching any funds. -- **A deadline field.** The signature carries a `deadline` (unix timestamp). Expired payloads revert with `DeadlineExpired()`. -- **Chain + deployment binding.** The signed digest always includes `block.chainid` and `address(this)`. A payload signed for one deployment cannot be replayed on a different chain or a different deployment of the same contract. +1. User approves AllowanceHolder (not OpenRouter). +2. User calls `AllowanceHolder.exec(operator, token, amount, target, data)` with `target = OpenRouter` and `data` encoding one of the router entrypoints. +3. AllowanceHolder forwards the call and appends the user address to calldata (ERC-2771 style). OpenRouter’s `_msgSender()` resolves to that user. +4. `_pullFromUser` calls `AllowanceHolder.transferFrom` and reverts with `CallerNotSignedUser()` unless `_msgSender() == input.user`. -The signature itself is a plain personal_sign (`\x19Ethereum Signed Message:\n32` prefix, 65-byte `r,s,v`) over `keccak256(abi.encode(chainid, address(this), executionPayload))`. This matches the scheme used in the marketplace `Solver` and `StakedRouterReceiver` contracts. +Native token input skips AllowanceHolder pull: the caller must forward sufficient `msg.value` on the outer transaction. -```solidity -// src/common/OpenRouterAuthBase.sol — `_verifyAndConsume` -if (AuthenticationLib.authenticate(digest, signature) != OPEN_ROUTER_SIGNER) { - assembly { - mstore(0x00, 0x815e1d64) // InvalidSigner() - revert(0x1c, 0x04) - } -} +`AllowanceHolderContext` also implements a harmless `balanceOf` so AllowanceHolder’s confused-deputy probe succeeds (same pattern as 0x Settler + AH). -assembly { - mstore(0, nonce) - mstore(0x20, nonceUsed.slot) - let dataSlot := keccak256(0, 0x40) - if and(sload(dataSlot), 0xff) { - mstore(0x00, 0x756688fe) // InvalidNonce() - revert(0x1c, 0x04) - } - sstore(dataSlot, 0x01) -} -``` +--- -The contract has no reentrancy guard, matching `Solver` and `StakedRouterReceiver`. The combination of a fresh nonce per call and a signature that covers the entire payload is the security boundary. +## External entrypoints ---- +| Function | Purpose | +|----------|---------| +| `swap` | Same-chain: pull → optional pre/post fee → swap → deliver output to `receiver` | +| `swapAndBridge` | Cross-chain: pull → optional pre/post swap fee → swap (output stays on router) → bridge | +| `bridge` | Direct bridge: pull → optional pre-bridge fee → bridge (amount baked into calldata) | +| `performActions` | Generic action loop with optional returndata splices | +| `rescueFunds` | Owner `RESCUE_ROLE` recovery of stuck tokens (operational, not a security boundary) | -## v1 — OpenRouter (monolithic) +Each structured entrypoint emits `RequestExecuted(bytes32 quoteId)` for off-chain correlation. `quoteId` is caller-defined; the contract does not validate it. -**File:** [`src/monolithic/OpenRouter.sol`](src/monolithic/OpenRouter.sol). AllowanceHolder variant: [`src/monolithic/OpenRouterAH.sol`](src/monolithic/OpenRouterAH.sol). +--- -This version encodes the full execution pipeline directly in the contract. The steps are explicit, ordered, and named. The signed payload is a single `Execution` struct: +## Structured routes — structs ```solidity -struct Execution { +struct InputData { address user; address inputToken; uint256 inputAmount; +} - address preFeeReceiver; // address(0) to skip - uint256 preFeeAmount; // taken in inputToken, before swap - - address swapTarget; // address(0) to skip swap entirely - address swapApprovalSpender; - address swapOutputToken; - uint256 swapValue; - uint256 swapMinOutput; - bytes swapData; - - address postFeeReceiver; // address(0) to skip - uint256 postFeeAmount; // taken in finalToken, after swap +struct FeeData { + address receiver; + uint256 amount; // 0 skips fee collection +} - address bridgeTarget; - address bridgeApprovalSpender; - uint256 bridgeValue; - bytes bridgeData; - uint256[] bridgeAmountPositions; // byte offsets where finalAmount is written +struct SwapData { + address target; + address approvalSpender; + address outputToken; + uint256 value; + uint256 minOutput; + uint256 returnDataWordOffset; // word index when using returndata output mode +} - uint256 nonce; - uint256 deadline; +struct BridgeData { + address target; + address approvalSpender; + uint256 value; // static msg.value addend (see BRIDGE_VALUE flag) } ``` -The contract `performExecution` function walks through this struct in a fixed order: +### `swap` -1. Pull `inputAmount` of `inputToken` from `user` (ERC20 `transferFrom` into the contract). -2. If `preFeeAmount > 0`, send that amount to `preFeeReceiver` immediately. -3. If `swapTarget != address(0)`, take a pre-swap balance snapshot of `swapOutputToken`, call the swap target, measure the balance delta, enforce `delta >= swapMinOutput`. The delta becomes `finalAmount` and `swapOutputToken` becomes `finalToken`. If there is no swap, `finalToken = inputToken` and `finalAmount = inputAmount - preFeeAmount`. -4. If `postFeeAmount > 0`, send that amount from `finalToken` to `postFeeReceiver`. -5. Write `finalAmount` into `bridgeData` at every byte offset in `bridgeAmountPositions` using an in-place `mstore`. This is the same pattern as `GenericStakedRoute.executeData`: +1. Pull `inputAmount` of `inputToken` from `user`. +2. If `fee.amount > 0` and **pre-fee** (`flags & 0x01 == 0`): transfer fee in input token, swap the remainder. +3. Approve `swapData.approvalSpender` when needed (max allowance, only if current allowance is insufficient). +4. Execute swap via `_execSwap` (see flags below). +5. Enforce `finalAmount >= swapData.minOutput` on **gross** swap output. +6. If **post-fee** (`flags & 0x01 != 0`): swap output lands on the router; fee is taken from output token; net is sent to `receiver`. +7. If **pre-fee / no fee**: swap calldata must send tokens **directly to `receiver`**; the router never holds swap output. -```solidity -// src/common/lib/BytesSpliceLib.sol — `spliceWord`, called for each position -assembly ("memory-safe") { - mstore(add(add(data, 0x20), position), word) -} -``` +### `swapAndBridge` -6. If `bridgeApprovalSpender != address(0)`, approve it for `finalAmount`. -7. Call `bridgeTarget` with the patched `bridgeData`, forwarding `bridgeValue` ETH. Any revert bubbles up with its original error data. +Same pull / pre-fee / swap / post-fee logic as above, but swap output **always** remains on `address(this)` for bridging. Then `_doBridge` splices the post-fee amount into bridge calldata (when flagged), approves the bridge spender, and calls the bridge target. -**When to use this.** Routes where the shape of the flow is always the same: pull → optional pre-fee → optional swap → optional post-fee → bridge. The contract knows the meaning of every field and enforces sensible preconditions (e.g. `finalAmount` cannot underflow below a fee). Adding a step that does not fit this shape — like a second bridge call, a pre-swap approval to a different address, or an intermediate hop — is not possible without deploying a new version of the contract. +### `bridge` -**AllowanceHolder variant (`OpenRouterAH`).** Instead of pulling with ERC20 `transferFrom` from the user to the router, the pull step calls 0x `AllowanceHolder.transferFrom` so funds move under that contract’s transient allowance (user approves AllowanceHolder, user calls `AllowanceHolder.exec` with `target = this router` and calldata invoking `performExecution`). The AH entry decodes `_msgSender()` as the original user appended by AllowanceHolder; `_pullFromUser` requires `_msgSender() == user`, so only the signer-named user matches the ephemeral allowance binding. Like Settler + AH patterns, `AllowanceHolderContext` exposes a harmless `balanceOf` on the router so AllowanceHolder’s confused-deputy probe succeeds; the rest of the pipeline is unchanged. +No swap. Pull → optional pre-bridge fee in input token → approve bridge spender → call bridge with `bridgeCallData` **unchanged**. ---- +Because `finalAmount = inputAmount - fee` is known up front, the caller must **bake the bridge amount into `bridgeCallData`** before submission. There is no runtime calldata splice on this path. -## v2 — OpenRouterModular (generic actions + returndata splicing) +--- -**File:** [`src/modular/OpenRouterModular.sol`](src/modular/OpenRouterModular.sol). AllowanceHolder variant: [`src/modular/OpenRouterModularAH.sol`](src/modular/OpenRouterModularAH.sol). +## Packed `flags` (structured routes) -This version removes all domain-specific knowledge from the contract. The only signed payload is a list of `Action`s: +One `uint256` packs switches for `swap` and `swapAndBridge` (not used by `bridge` or `performActions`): -```solidity -struct Action { - CallType callType; // CALL, DELEGATECALL, or STATICCALL - address target; - uint256 value; // ETH forwarded; must be zero for non-CALL - bytes data; // base calldata, may be partially overwritten by splices - Splice[] splices; // applied to data before this action runs -} +| Bits | Mask | Meaning | +|------|------|---------| +| 0 | `0x01` | Post-swap fee: fee taken from output token after swap. Clear = pre-swap fee from input. | +| 1 | `0x02` | Swap output via `balanceOf` delta on `outputToken`. Clear = decode return word at `swapData.returnDataWordOffset`. | +| 2 | `0x04` | Bridge `msg.value = finalAmount + bridgeData.value` (e.g. LayerZero `nativeFee` addend in `bridgeData.value`). Clear = `bridgeData.value` only. | +| 3 | `0x08` | Splice `finalAmount` into bridge calldata at byte offset `(flags >> 16) & 0xffff`. | +| 16–31 | — | Byte offset for bridge amount splice when bit 3 is set. | -struct Splice { - uint256 srcOffset; // byte offset within the *previous* action's returndata - uint256 dstOffset; // byte offset within this action's data - uint256 length; // how many bytes to copy -} -``` +Common combinations: -The loop is: +| `flags` | Fee | Swap output | Bridge `msg.value` | +|---------|-----|-------------|-------------------| +| `0x00` | pre | returndata | `bridgeData.value` | +| `0x01` | post | returndata | `bridgeData.value` | +| `0x02` | pre | balance delta | `bridgeData.value` | +| `0x03` | post | balance delta | `bridgeData.value` | +| `0x04` | pre | returndata | `finalAmount + bridgeData.value` | -``` -prevReturn = empty bytes -for each action: - apply all splices (copy ranges from prevReturn into action.data) - dispatch the call - prevReturn = returndata from this call -``` +Add `0x08` and set bits 16–31 when bridge calldata needs the live swap output at a fixed offset (same idea as `GenericStakedRoute` / `BytesSpliceLib.spliceWord`). -**How splicing works.** The problem it solves: after a swap, the exact output amount is not known until runtime. The signed `data` for the subsequent bridge call contains a placeholder value at some byte offset. A splice says "before you make this call, copy bytes `[srcOffset, srcOffset+length)` from what the previous call returned into `data[dstOffset, dstOffset+length)`". After the copy, the call is made with the updated data. +--- -A concrete example: suppose action 0 is a STATICCALL to `balanceOf(address(this))` on the output token. Its returndata is 32 bytes encoding the current balance. Action 1 is the bridge call. Its `splices` list contains one entry: `{ srcOffset: 0, dstOffset: 68, length: 32 }`, which says "take the 32-byte balance from action 0's returndata and write it at byte 68 of the bridge calldata". When action 1 runs, its calldata already has the live balance written in. +## Generic routes — `performActions` -Under the hood, the copy uses `mcopy` (Cancun, EIP-5656): +For flows that need extra hops, manipulator contracts, or multiple splices into one calldata blob, use the modular path: ```solidity -// BytesSpliceLib.spliceBytes -assembly ("memory-safe") { - mcopy( - add(add(dst, 0x20), dstOffset), - add(add(src, 0x20), srcOffset), - length - ) +struct Action { + uint256 actionInfo; // packed call metadata + bytes data; + uint256[] splices; // packed splice descriptors } + +enum CallType { CALL, STATICCALL, CALL_WITH_NATIVE } ``` -Both source and destination offsets are bounds-checked before the copy; zero-length splices are rejected. +### `actionInfo` layout -**Security note on splices.** The base `data` for every action is part of the signed payload. A splice can only overwrite bytes within that signed data — it cannot change the call target, add extra function arguments, or replace the entire calldata. An adversarial return value can only influence the specific byte ranges the signer chose to splice. The signer controls which offsets are writable by choosing which splices to include. +```text +bits 0–7 : CallType (CALL = 0, STATICCALL = 1, CALL_WITH_NATIVE = 2) +bit 8 : store returndata for later splices +bits 16+ : target address (uint160, shifted left 16) +``` -**DELEGATECALL support.** When `callType == DELEGATECALL`, the call runs with this contract's storage and `address(this)`. This is how you plug in a separate implementation contract (analogous to how `BungeeGateway` delegates to its impl) without giving it the whitelist status required by the gateway. Caution applies: a delegatecall target can modify the contract's storage, so only trusted, audited implementation contracts should be used in this slot. +### `splices[]` entry layout -**When to use this.** Any route where the exact amount flowing between steps is not known until runtime and must be piped into the next step's calldata. The canonical motivating case is an integration like Across, where two separate fields in the bridge calldata both need to reflect the swap output amount. With `GenericStakedRoute` you can only patch one offset; with this contract you declare as many splices as needed, each targeting a different offset. +Each `splices[j]` is one `uint256`: -**AllowanceHolder variant (`OpenRouterModularAH`).** The action loop is identical after verification: no built-in pull. You choose how to compose an AllowanceHolder `transferFrom` (or delegatecall shim) as one or more ordinary `CALL` actions signed with everything else; `performExecutionAH` wraps that by binding the signature to `(chainId, this, signedUser, exec)` instead of omitting `signedUser`. It asserts `_msgSender() == signedUser` so nobody can burn another user’s nonce by submitting their payload inside a stranger’s `AH.exec`; real fund safety still comes from AllowanceHolder’s operator/owner/token scoping; `AllowanceHolderContext` only supplies the dummy `balanceOf` for AH’s probing. +```text +sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) +``` ---- +Before action `i` runs, each splice copies `length` bytes from `results[sourceActionIndex]` at `srcOffset` into this action’s `data` at `dstOffset` (via `mcopy`). `sourceActionIndex` must be **strictly less than** `i` or the call reverts with `FutureSplice`. -## v3 — OpenRouterMinimal (generic actions, no splicing) +`CALL_WITH_NATIVE`: first 32 bytes of `data` are `msg.value`; remaining bytes are calldata. -**File:** [`src/minimal/OpenRouterMinimal.sol`](src/minimal/OpenRouterMinimal.sol). AllowanceHolder variant: [`src/minimal/OpenRouterMinimalAH.sol`](src/minimal/OpenRouterMinimalAH.sol). +There is **no built-in pull** in `performActions`. Compose AllowanceHolder `transferFrom` (or other setup) as ordinary actions in the signed/off-chain-built sequence. -This version is the stripped-down sibling of v2. The `Action` struct has no `splices` field: +--- -```solidity -struct Action { - CallType callType; - address target; - uint256 value; - bytes data; // used exactly as signed; never mutated -} -``` +## Internal helpers (shared behavior) -The loop dispatches each action with its signed data verbatim and discards the return value. There is no mechanism to move output from one call into the input of the next. +- **`_pullFromUser`** — AllowanceHolder ERC-20 pull or native `msg.value` check. +- **`_execSwap`** — balance-delta or returndata word decode; enforces `minOutput` at the entrypoint. +- **`_doBridge`** — optional `BytesSpliceLib.spliceWord` on bridge calldata, approval, then `_doCall`. +- **`_performActions`** — splice loop + low-level `call` / `staticcall` with bubbled revert data. -``` -for each action: - dispatch the call (no splice step) - discard returndata -``` +Approvals use Solady `safeApproveWithRetry` to `type(uint256).max` only when current allowance is below the needed amount. -**How steps communicate without splicing.** They don't — at least not through the router. Instead, the called contracts are responsible for reading whatever state they need at runtime. The most common pattern is pre/post balance accounting: the bridge target (e.g. a `GenericStakedRoute`-style contract or `BungeeApproveAndBridge`) calls `balanceOf(address(this))` itself to discover how much of the token it holds after the previous step deposited it, rather than receiving the amount as an argument. +--- -This is exactly how `BaseRouterSingleOutput` works: it measures the swap output by comparing balances before and after the swap call, then passes the delta to `_execute`. With v3, that accounting logic lives inside the called contracts, not in the router. +## Choosing structured vs generic -**When to use this.** Routes where every action is self-contained — the called contracts know what token to look at, query their own balance, and use that as their amount. This covers most `GenericStakedRoute` flows today, since those contracts already contain the offset-patching and balance-reading logic. v3 is the right choice when you do not need cross-action data passing at the router layer, and you want the smallest possible trusted surface in the router contract itself. +| Use | When | +|-----|------| +| `swap` | Same-chain DEX with optional fee; output to a known `receiver`. | +| `swapAndBridge` | Swap then bridge; runtime bridge amount and/or native bridge value from swap output. | +| `bridge` | No swap; amount and calldata fixed before the tx. | +| `performActions` | Multi-step or integration-specific flows (e.g. swap → manipulator → splice into `SpokePool.deposit`). | -**AllowanceHolder variant (`OpenRouterMinimalAH`).** Same idea as the modular AH: use `performExecutionAH` plus `AllowanceHolderContext`’s `balanceOf`; sign over `signedUser` and require `_msgSender() == signedUser` for nonce-binding; compose the AH pull as ordinary actions in `exec.actions`. +Structured entrypoints keep audit surface small: linear control flow and explicit preconditions. `performActions` is the escape hatch when the pipeline is not pull → fee → swap → bridge. --- -## Choosing between them +## Security model (summary) -The three versions exist on a spectrum from "the contract knows everything" to "the contract knows nothing except who signed". +| Enforced on-chain | Not enforced | +|-------------------|--------------| +| `_msgSender() == user` on ERC-20 pull | Backend signature / nonce / deadline | +| `minOutput` after swap | That calldata matches user intent | +| Splice bounds and `FutureSplice` | That `performActions` targets are benign | +| AllowanceHolder scoping for pulls | Router must not accumulate balances or receive direct user approvals | -**v1** is the right choice when you want the router to be the authoritative record of what the flow does — you can read one struct and understand the entire execution. The cost is that every variant of the flow (different fee timing, multi-hop bridge, etc.) needs a new contract or a new version. It is also the easiest to audit because the control flow is linear and every named step has an explicit precondition check. - -**v2** is the right choice when you need to pipe outputs between steps in ways the called contracts cannot handle themselves. The key example is when a bridge call has two separate amount fields that both need to reflect the swap output — one splice entry per field, both handled in one atomic execution. The contract becomes a thin orchestrator and the "business logic" of each step lives in the action targets. - -**v3** is the right choice when the called contracts already handle their own amount discovery (balance-check style) and you just need a trusted sequencer that ensures the actions run in the signed order. It is the most gas-efficient version at the router layer because there is no splice computation overhead, and it is the easiest to build new action targets for because those targets do not need to conform to any returndata shape. +`performActions` is **public**. Any caller can execute arbitrary action lists. Operational safety depends on users only approving AllowanceHolder, never OpenRouter directly, and on backend/frontend validating routes before `AllowanceHolder.exec`. See [`OPENROUTER_ASSUMPTIONS.md`](OPENROUTER_ASSUMPTIONS.md) for the full assumption set. --- -## Shared libraries - -All live under `src/common/`. +## Shared libraries (`src/common/`) -**`OpenRouterAuthBase.sol`** — abstract base all three inherit. Owns the signer address, the nonce mapping, and `_verifyAndConsume`. +| Module | Role | +|--------|------| +| `CurrencyLib` | Native sentinel + transfers / `balanceOf` | +| `BytesSpliceLib` | `spliceWord` for bridge calldata; `mcopy`-based `spliceBytes` in modular path | +| `RescueFundsLib` | `rescueFunds` implementation | +| `AllowanceHolderContext` | `_msgSender()` / dummy `balanceOf` for AH | -**`lib/AuthenticationLib.sol`** — personal_sign recovery (`\x19Ethereum Signed Message:\n32` + ecrecover). Matches `marketplace/src/lib/AuthenticationLib.sol` exactly. +`OpenRouterAuthBase` and signed-router variants are **not** used by this contract. -**`lib/CurrencyLib.sol`** — wraps Solady `SafeTransferLib` with a native token shortcut (address `0xEee...EEe`), identical in spirit to the marketplace `CurrencyLib`. - -**`lib/BytesSpliceLib.sol`** — used by v1 (writing `finalAmount` to multiple positions in bridge calldata) and v2 (the per-splice `mcopy`). Exposes `spliceWord` (32-byte in-place overwrite, same assembly as `GenericStakedRoute`), `spliceWords` (repeat for multiple positions), and `spliceBytes` (arbitrary-length copy via `mcopy`, bounds-checked). - -**`allowance/AllowanceHolderContext.sol`**, **`interfaces/IAllowanceHolder.sol`** — imported only by the `*AH` contracts in each variant folder. +--- +## Backend and tests +ABI encoders (update if the Solidity ABI changes): +- `bungee-backend/src/modules/dex/utils.ts` — `swap`, AllowanceHolder `exec` +- `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts` — `bridge`, `swapAndBridge` -0. AllowanceHolder -1. OpenRouter -> Fee Transfer -> -2. OpenRouter (modify input) -> Swap execution -> OpenRouter (modify input) -> -3. AcrossManipulator -> OpenRouter (modify input) -> -4. SpokePool +Tests: -0. AllowanceHolder -1. OpenRouter -> Fee Transfer -> -2. OpenRouter (modify input) -> Swap execution -> OpenRouter (modify input) -> -3. AcrossRouter(amount, AcrossBridgeData) -> (modify SpokePool input with output ) -> SpokePool +- `test/combined/OpenRouterV2Unchecked*.t.sol` — unit tests against `src/OpenRouter.sol` +- `test/poc/*OpenRouterPoC.t.sol` — fork PoCs using `performActions` + manipulators -0. AllowanceHolder -1. AcrossRouter - should have all the fee, swap, bridge code in this \ No newline at end of file +Deploy: `scripts/deploy/deployOpenRouter.ts` (`constructor(address _owner)` grants `RESCUE_ROLE`). From 7ba76fe69603dfdaa8cb33ddeca4347ad17e6a99 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 20 May 2026 13:51:06 +0530 Subject: [PATCH 64/69] feat: create3 --- scripts/deploy/create3.ts | 39 +++++++++++++++++++++++ scripts/deploy/deployOpenRouter.ts | 51 ++++++++++++++++++++++++------ 2 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 scripts/deploy/create3.ts diff --git a/scripts/deploy/create3.ts b/scripts/deploy/create3.ts new file mode 100644 index 0000000..4009a7d --- /dev/null +++ b/scripts/deploy/create3.ts @@ -0,0 +1,39 @@ +import { Log, TransactionReceipt, keccak256, toUtf8Bytes } from 'ethers'; + +// CreateX factory — https://createx.rocks/ +export const CREATE_X_FACTORY = '0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed'; + +export const Create3ABI = [ + 'function computeCreate2Address(bytes32,bytes32,address) view returns (address)', + 'function deployCreate2(bytes32,bytes) payable returns (address)', + 'function computeCreate3Address(bytes32,address) view returns (address)', + 'function deployCreate3(bytes32,bytes) payable returns (address)', +]; + +const Create3ContractCreationEvent = 'ContractCreation(address)'; +const Create3ContractCreationEventTopicHash = keccak256( + toUtf8Bytes(Create3ContractCreationEvent), +); + +/** + * Reads the deployed contract address from a CreateX CREATE3 deployment receipt. + */ +export function decodeCreate3DeploymentFromTxReceipt(params: { + receipt: TransactionReceipt; +}): string | null { + const { receipt } = params; + const filteredLogs: Log[] = receipt.logs.filter((log: Log) => + log.topics.includes(Create3ContractCreationEventTopicHash), + ); + + if (filteredLogs.length === 0) { + return null; + } + + const eventData = filteredLogs[0].topics[1]; + if (!eventData) { + return null; + } + + return '0x' + eventData.slice(26); +} diff --git a/scripts/deploy/deployOpenRouter.ts b/scripts/deploy/deployOpenRouter.ts index 6afcb60..624ba30 100644 --- a/scripts/deploy/deployOpenRouter.ts +++ b/scripts/deploy/deployOpenRouter.ts @@ -1,21 +1,25 @@ /** - * Deployment script for OpenRouter. + * Deployment script for OpenRouter via CreateX CREATE3. * * Usage: * npx hardhat run scripts/deploy/deployOpenRouter.ts --network * * Required env vars: * DEPLOYER_PRIVATE_KEY — deployer wallet private key - */ import hre from 'hardhat'; import { ethers } from 'hardhat'; +import { keccak256, toUtf8Bytes } from 'ethers'; +import { + CREATE_X_FACTORY, + Create3ABI, + decodeCreate3DeploymentFromTxReceipt, +} from './create3'; async function main() { const [deployer] = await ethers.getSigners(); const networkName = hre.network.name; - const owner = deployer.address; console.log('Deployer: ', deployer.address); @@ -23,11 +27,42 @@ async function main() { console.log('Network: ', networkName); console.log(''); - console.log('Deploying OpenRouter...'); + const constructorArgs = { _owner: owner }; + console.log('constructorArgs', constructorArgs); + + const create3Factory = new ethers.Contract( + CREATE_X_FACTORY, + Create3ABI, + deployer, + ); + const factory = await ethers.getContractFactory('OpenRouter'); - const router = await factory.deploy(owner); - await router.waitForDeployment(); - const routerAddress = await router.getAddress(); + const deployTransaction = await factory.getDeployTransaction(owner); + + const saltText = 'OpenRouter' + 1; + const salt = keccak256(toUtf8Bytes(saltText)); + + const deployAddress = await create3Factory.deployCreate3.staticCall( + salt, + deployTransaction.data, + ); + console.log('Contract address will be:', deployAddress); + + console.log('Deploying OpenRouter via CREATE3...'); + const create3Deployment = await create3Factory.deployCreate3( + salt, + deployTransaction.data, + ); + console.log('CREATE3 deployment tx:', create3Deployment.hash); + + const receipt = await create3Deployment.wait(); + const routerAddress = decodeCreate3DeploymentFromTxReceipt({ receipt }); + if (!routerAddress) { + throw new Error( + 'OpenRouter address not found in CREATE3 deployment receipt', + ); + } + console.log('OpenRouter deployed to:', routerAddress); console.log('\n=== Deployment Summary ==='); @@ -35,10 +70,8 @@ async function main() { const chainId = (await ethers.provider.getNetwork()).chainId; if (chainId !== 31337n) { - // sleep for 5secs before verification attempt await new Promise((resolve) => setTimeout(resolve, 5000)); - // run verification await hre.run('verify:verify', { address: routerAddress, constructorArguments: [owner], From 4a7b7325d9d1b1a063656c914a53bc2ccb777855 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 20 May 2026 21:07:32 +0530 Subject: [PATCH 65/69] chore: comments --- OPENROUTER.md | 65 ++++++++++++++++++++---- src/OpenRouter.sol | 124 +++++++++++++++++++++++++++++++++------------ 2 files changed, 148 insertions(+), 41 deletions(-) diff --git a/OPENROUTER.md b/OPENROUTER.md index 17b94a4..ca6020c 100644 --- a/OPENROUTER.md +++ b/OPENROUTER.md @@ -141,9 +141,9 @@ For flows that need extra hops, manipulator contracts, or multiple splices into ```solidity struct Action { - uint256 actionInfo; // packed call metadata - bytes data; - uint256[] splices; // packed splice descriptors + uint256 actionInfo; // packed call metadata (see below) + bytes data; // calldata; patched by splices before the call + uint256[] splices; // packed splice descriptors (see below) } enum CallType { CALL, STATICCALL, CALL_WITH_NATIVE } @@ -151,23 +151,70 @@ enum CallType { CALL, STATICCALL, CALL_WITH_NATIVE } ### `actionInfo` layout +One `uint256` per action. All fields are uint64-safe except `target` (uint160). + +| Bits | Field | Type | Meaning | +|------|-------|------|---------| +| 0–2 | `callType` | `uint8` | `CallType`: `CALL` (0), `STATICCALL` (1), `CALL_WITH_NATIVE` (2) | +| 3–7 | — | — | reserved (0) | +| 8 | `storeResult` | `bool` | When set, returndata is saved to `results[i]` even on success so later actions can splice from it | +| 9–15 | — | — | reserved (0) | +| 16–175 | `target` | `address` | Callee address (`uint160`, shifted left 16) | +| 176–255 | — | — | reserved (0) | + +Packing (matches `packActionInfo` in [`scripts/e2e/utils/modularActionsBuilder/index.js`](scripts/e2e/utils/modularActionsBuilder/index.js)): + ```text -bits 0–7 : CallType (CALL = 0, STATICCALL = 1, CALL_WITH_NATIVE = 2) -bit 8 : store returndata for later splices -bits 16+ : target address (uint160, shifted left 16) +callType | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16) ``` +`CALL_WITH_NATIVE`: first 32 bytes of `data` are `msg.value`; remaining bytes are calldata. + ### `splices[]` entry layout -Each `splices[j]` is one `uint256`: +Each `splices[j]` is one `uint256` describing a byte-range copy from a prior action’s returndata into this action’s `data`. Offsets are into the **payload** bytes (the bytes-array contents), not including Solidity’s 32-byte length prefix. + +| Bits | Field | Type | Meaning | +|------|-------|------|---------| +| 0–63 | `sourceActionIndex` | `uint64` | Index of the prior action whose returndata is the copy source | +| 64–127 | `srcOffset` | `uint64` | Byte offset into `results[sourceActionIndex]` payload | +| 128–191 | `dstOffset` | `uint64` | Byte offset into this action’s `data` payload | +| 192–255 | `length` | `uint64` | Number of bytes to copy (must be > 0) | + +Packing (matches `packSpliceInfo` in the modular actions builder): ```text sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) ``` -Before action `i` runs, each splice copies `length` bytes from `results[sourceActionIndex]` at `srcOffset` into this action’s `data` at `dstOffset` (via `mcopy`). `sourceActionIndex` must be **strictly less than** `i` or the call reverts with `FutureSplice`. +Before action `i` runs, each splice copies `length` bytes from `results[sourceActionIndex]` at `srcOffset` into this action’s `data` at `dstOffset` (via `mcopy`). Constraints enforced on-chain: -`CALL_WITH_NATIVE`: first 32 bytes of `data` are `msg.value`; remaining bytes are calldata. +- `sourceActionIndex < i` — otherwise `FutureSplice` +- `srcOffset + length <= source.length` and `dstOffset + length <= data.length` — otherwise `SpliceOutOfBounds` +- The source action must have `storeResult` set (bit 8 of its `actionInfo`); the JS builder sets this automatically when a splice references that action + +**Destination offset conventions** (builder helpers in `modularActionsBuilder/index.js`): + +| Helper | `dstOffset` for… | +|--------|------------------| +| `spliceArg(n, source)` | ABI arg `n` in a normal call: `4 + n * 32` (past the 4-byte selector) | +| `valueFrom(source)` / `spliceNativeValue` | Leading value word of `CALL_WITH_NATIVE`: `0` | +| `splicePayloadWord(off, source)` | Payload of `CALL_WITH_NATIVE`: `32 + off` | +| `patchWord(off, source)` | Absolute payload offset `off` | + +Example: splice the first 32 bytes of action 0’s returndata into byte offset 132 of action 2’s calldata: + +```js +const { packSpliceInfo } = require("./scripts/e2e/utils/modularActionsBuilder/index"); + +packSpliceInfo({ + sourceActionIndex: 0, + srcOffset: 0, + dstOffset: 132, + length: 32, +}); +// => 0n | (0n << 64n) | (132n << 128n) | (32n << 192n) +``` There is **no built-in pull** in `performActions`. Compose AllowanceHolder `transferFrom` (or other setup) as ordinary actions in the signed/off-chain-built sequence. diff --git a/src/OpenRouter.sol b/src/OpenRouter.sol index 858c00d..32abfea 100644 --- a/src/OpenRouter.sol +++ b/src/OpenRouter.sol @@ -57,8 +57,41 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { } struct Action { + /// @dev Packed call metadata. Decode with masks/shifts below; encode with + /// `callType | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16)`. + /// + /// Bit layout (least significant bits first): + /// bits 255..160 : reserved (0) + /// bits 159..16 : target address (uint160, left-aligned in this field) + /// bit 8 : storeResult — when set, returndata is saved to `results[i]` + /// even on success so later actions can splice from it + /// bits 7..3 : reserved (0) + /// bits 2..0 : CallType — CALL (0), STATICCALL (1), CALL_WITH_NATIVE (2) + /// + /// CALL_WITH_NATIVE: first 32 bytes of `data` are forwarded as `msg.value`; + /// the remaining bytes are the call payload. uint256 actionInfo; + /// @dev Calldata passed to the target. Splices from `splices[]` overwrite byte + /// ranges in a mutable memory copy before the external call runs. bytes data; + /// @dev Packed splice descriptors applied to `data` before the call. + /// Each entry is one `uint256` with four uint64 fields (see layout below). + /// Encode with `packSpliceInfo` in `scripts/e2e/utils/modularActionsBuilder/index.js`. + /// + /// Per-entry bit layout (least significant bits first): + /// bits 255..192 : length — number of bytes to copy (must be > 0) + /// bits 191..128 : dstOffset — byte offset into this action's `data` payload + /// (skips the bytes-array length word; for CALL_WITH_NATIVE, + /// offset 0 is the value word, offset 32 is payload start) + /// bits 127..64 : srcOffset — byte offset into `results[sourceActionIndex]` + /// payload (same length-prefix convention) + /// bits 63..0 : sourceActionIndex — index of a prior action (< current index) + /// + /// Packing formula: + /// sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) + /// + /// The source action must have bit 8 set in `actionInfo` (storeResult); the JS + /// builder sets this automatically when a splice references that action. uint256[] splices; } @@ -467,11 +500,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { /** * @dev Executes `actions` in order, applying returndata splices before each call. - * @dev actionInfo layout: - * - bits 0–7: call type (`CallType`) - * - bit 8: store returndata - * - bits 16+: target address - * splices[j` packs source index, src/dst byte offsets, and length. + * @dev See `Action` for `actionInfo` and `splices[]` bit layouts. * @param actions Ordered list of actions to run. */ function _performActions(Action[] calldata actions) internal { @@ -482,21 +511,25 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { Action calldata action = actions[i]; bytes memory callData = action.data; + // Patch callData with slices of prior action returndata. uint256 splicesLength = action.splices.length; for (uint256 j; j < splicesLength;) { uint256 spliceInfo = action.splices[j]; - uint256 sourceActionIndex = uint64(spliceInfo); + uint256 sourceActionIndex = uint64(spliceInfo); // first 64 bits: index of the prior action to read returndata from. if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); - uint256 srcOffset = uint64(spliceInfo >> 64); - uint256 dstOffset = uint64(spliceInfo >> 128); - uint256 length = spliceInfo >> 192; + uint256 srcOffset = uint64(spliceInfo >> 64); // Next 64 bits: byte offset into source returndata + uint256 dstOffset = uint64(spliceInfo >> 128); // Next 64 bits: byte offset into next action's data + uint256 length = spliceInfo >> 192; // Top 64 bits: number of bytes to copy + + // Fetch source action returndata bytes memory source = results[sourceActionIndex]; if (srcOffset + length > source.length || dstOffset + length > callData.length) { revert SpliceOutOfBounds(i, j); } assembly ("memory-safe") { + // copy `length` bytes from `source returndata starting from `srcOffset` to `callData` starting from `dstOffset` mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) } @@ -505,14 +538,16 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { } } + // Parse actionInfo bool success; uint256 actionInfo = action.actionInfo; - bool storeResult = (actionInfo & 0xff00) != 0; - uint256 callType = actionInfo & 0xff; - address target = address(uint160(actionInfo >> 16)); + bool storeResult = (actionInfo & 0xff00) != 0; // Bit 8: persist returndata if set + uint256 callType = actionInfo & 0xff; // Bits 0–7: specify CallType + address target = address(uint160(actionInfo >> 16)); // Bits 16+: target address if (callType == uint256(CallType.STATICCALL)) { assembly ("memory-safe") { + // staticcall without copying return data by default success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) } } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { @@ -520,25 +555,32 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { uint256 callValue; uint256 payloadLength = callData.length - 32; assembly ("memory-safe") { - callValue := mload(add(callData, 0x20)) - success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) + // regular call with value forwarded without copying return data by default + callValue := mload(add(callData, 0x20)) // CALL_WITH_NATIVE prepends a 32-byte wei amount before the actual calldata payload. + success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) // skips first two bytes to reach actuall calldata } } else { assembly ("memory-safe") { + // regular call with zero value forwarded without copying return data by default success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) } } + // Capture returndata on failure (for revert reason) or when explicitly requested. if (!success || storeResult) { bytes memory ret; assembly ("memory-safe") { + // prep return / revert data let returnDataSize := returndatasize() ret := mload(0x40) - mstore(ret, returnDataSize) - returndatacopy(add(ret, 0x20), 0, returnDataSize) - mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) + mstore(ret, returnDataSize) // write length prefix on free-mem pointer + returndatacopy(add(ret, 0x20), 0, returnDataSize) // copy returndata after length + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) // Advance free pointer to next 32-byte boundary: (ret + 0x20 + size + 31) and clear last 5 bits with not(0x1f) } + // if any call was failed, revert with the returndata if (!success) revert CallFailed(i, ret); + + // else, save returndata to results array results[i] = ret; } unchecked { @@ -575,17 +617,32 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { // Call AllowanceHolder.transferFrom() address allowanceHolder = address(ALLOWANCE_HOLDER); assembly ("memory-safe") { + // Manually ABI-encode AllowanceHolder.transferFrom(address token, address owner, address recipient, uint256 amount) + // selector 0x15dacbea. Calldata is 0x84 (132) bytes and starts at ptr+0x1c (see last mstore below). + // + // The `shl(0x60, addr)` trick left-aligns a 20-byte address in a 32-byte word: the high 20 bytes + // hold the address and the trailing 12 bytes are zero, which simultaneously encodes the address AND + // provides the ABI zero-padding for the *next* field — so each shifted mstore clears the following + // field's padding without a separate write. + // + // Calldata layout relative to ptr+0x1c: + // [0..3] selector (0x15dacbea) + // [4..35] token (12-byte pad + 20-byte address) + // [36..67] owner/user (12-byte pad + 20-byte address) + // [68..99] recipient (12-byte pad + 20-byte address = address(this)) + // [100..131] amount (uint256) let ptr := mload(0x40) - mstore(add(0x80, ptr), amount) - mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears `recipient`'s padding + mstore(add(0x80, ptr), amount) // calldata[100..131]: amount (uint256, right-aligned) + mstore(add(0x60, ptr), address()) // calldata[68..99]: recipient = this contract (right-aligned, high 12 bytes are zero padding) + mstore(add(0x4c, ptr), shl(0x60, user)) // calldata[48..67]: user address; trailing 12 zero bytes fill calldata[68..79] (recipient padding) // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which // shifts the 20-byte address out of place and corrupts the calldata token. Same as // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. - mstore(add(0x2c, ptr), shl(0x60, token)) // clears `owner`'s padding (settler wording) - mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding + mstore(add(0x2c, ptr), shl(0x60, token)) // calldata[16..35]: token address; trailing 12 zero bytes fill calldata[36..47] (user padding) + mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector at calldata[0..3]; 12 zero bytes fill calldata[4..15] (token padding); calldata begins at ptr+0x1c if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { + // if call did not succeed, revert with the revert returndata let p := mload(0x40) returndatacopy(p, 0x00, returndatasize()) revert(p, returndatasize()) @@ -638,12 +695,13 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { if (!success) { bytes memory ret; assembly ("memory-safe") { + // prep and return revert data let returnDataSize := returndatasize() ret := mload(0x40) - mstore(ret, returnDataSize) - returndatacopy(add(ret, 0x20), 0, returnDataSize) - mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) - revert(add(ret, 0x20), mload(ret)) + mstore(ret, returnDataSize) // write length prefix on free-mem pointer + returndatacopy(add(ret, 0x20), 0, returnDataSize) // copy returndata after length + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) // bump free pointer + revert(add(ret, 0x20), mload(ret)) // bubbles up the original revert payload } } } @@ -664,22 +722,23 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { bool success; assembly ("memory-safe") { let ptr := mload(0x40) - calldatacopy(ptr, data.offset, data.length) - mstore(0x40, and(add(add(ptr, data.length), 0x1f), not(0x1f))) + calldatacopy(ptr, data.offset, data.length) // copy calldata slice to fresh memory (avoids redundant memory alloc) + mstore(0x40, and(add(add(ptr, data.length), 0x1f), not(0x1f))) // advance free pointer to next 32-byte boundary success := call(gas(), target, value, ptr, data.length, 0, 0) } if (!success || storeResult) { assembly ("memory-safe") { + // prep and return revert data let returnDataSize := returndatasize() ret := mload(0x40) - mstore(ret, returnDataSize) - returndatacopy(add(ret, 0x20), 0, returnDataSize) - mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) + mstore(ret, returnDataSize) // write length prefix on free-mem pointer + returndatacopy(add(ret, 0x20), 0, returnDataSize) // copy returndata after length + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) // bump free pointer } if (!success) { assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) + revert(add(ret, 0x20), mload(ret)) // bubble up the raw revert payload } } } @@ -696,6 +755,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { if (offset + 32 > ret.length) revert ReturnDataOutOfBounds(); assembly ("memory-safe") { + // read the word at the offset from return data word := mload(add(add(ret, 0x20), offset)) } } From e2a09a5d532f9a8b000419a2a36010ed8ecca415 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Wed, 20 May 2026 21:36:51 +0530 Subject: [PATCH 66/69] test: allowance holder pull fork gas test --- test/poc/OpenRouterAllowanceHolderFork.t.sol | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/poc/OpenRouterAllowanceHolderFork.t.sol diff --git a/test/poc/OpenRouterAllowanceHolderFork.t.sol b/test/poc/OpenRouterAllowanceHolderFork.t.sol new file mode 100644 index 0000000..abde3fe --- /dev/null +++ b/test/poc/OpenRouterAllowanceHolderFork.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; + +/// @dev No-op bridge target so `router.bridge` can complete after the pull. +contract NoopBridgeTarget { + function ping() external {} +} + +/// @notice Polygon fork: user funds + AH approval, entry via AllowanceHolder.exec, OpenRouter pulls via `_pullFromUser`. +contract OpenRouterAllowanceHolderForkTest is Test { + address internal constant POLYGON_USDC = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; + uint256 internal constant POLYGON_FORK_BLOCK = 86_816_149; + uint256 internal constant INPUT_AMOUNT = 100e6; + + address internal user; + + function setUp() public { + user = makeAddr("ahForkUser"); + } + + function test_fork_openRouter_bridge_pullsFromUserViaAllowanceHolder() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to run this fork test."); + return; + } + + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", POLYGON_FORK_BLOCK); + vm.createSelectFork(rpcUrl, forkBlock); + + Router router = new Router(address(this)); + NoopBridgeTarget noopBridge = new NoopBridgeTarget(); + + deal(POLYGON_USDC, user, INPUT_AMOUNT); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0, "router must not be pre-funded"); + + vm.prank(user); + ERC20(POLYGON_USDC).approve(address(ALLOWANCE_HOLDER), INPUT_AMOUNT); + + bytes memory routerCalldata = abi.encodeCall( + Router.bridge, + ( + keccak256("open-router-ah-fork"), + Router.InputData({user: user, inputToken: POLYGON_USDC, inputAmount: INPUT_AMOUNT}), + Router.FeeData({receiver: address(0), amount: 0}), + Router.BridgeData({target: address(noopBridge), approvalSpender: address(0), value: 0}), + abi.encodeCall(NoopBridgeTarget.ping, ()) + ) + ); + + // Runtime-only gas (excludes `new OpenRouter` / `new NoopBridgeTarget` above). + // Forge's per-test `gas:` figure still includes deployment; use this log for comparisons. + uint256 gasBeforeExec = gasleft(); + vm.prank(user); + IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( + address(router), POLYGON_USDC, INPUT_AMOUNT, payable(address(router)), routerCalldata + ); + uint256 runtimeGas = gasBeforeExec - gasleft(); + emit log_named_uint("runtime gas AH.exec -> router.bridge", runtimeGas); + + assertEq(ERC20(POLYGON_USDC).balanceOf(user), 0, "user balance"); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), INPUT_AMOUNT, "router pulled via AH"); + } +} From bce867e1f2c922c391eb48db1c0805180847ae7c Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 25 May 2026 18:58:12 +0530 Subject: [PATCH 67/69] chore: comments, function renames --- src/OpenRouter.sol | 21 ++++++++++----------- src/common/lib/BytesSpliceLib.sol | 4 +--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/OpenRouter.sol b/src/OpenRouter.sol index 32abfea..d9aa58d 100644 --- a/src/OpenRouter.sol +++ b/src/OpenRouter.sol @@ -315,7 +315,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { uint256 finalAmount = _swapBeforeBridge(flags, input, fee, swapData, swapCallData); // Execute bridge - _doBridge(swapData.outputToken, finalAmount, flags, bridgeData, bridgeCallData); + _execBridge(swapData.outputToken, finalAmount, flags, bridgeData, bridgeCallData); emit RequestExecuted(quoteId); } @@ -368,7 +368,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { } // Execute bridge - _doCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); + _execCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); emit RequestExecuted(quoteId); } @@ -399,7 +399,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { * @param fee Fee receiver and amount; `amount == 0` skips fee collection. * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. * @param swapCallData Calldata forwarded to `swapData.target`. - * @return finalAmount Swap output net of any post-swap fee, ready for `_doBridge`. + * @return finalAmount Swap output net of any post-swap fee, ready for `_execBridge`. */ function _swapBeforeBridge( uint256 flags, @@ -461,7 +461,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. * @param bridgeCallData Base bridge calldata; copied to memory when splicing is required. */ - function _doBridge( + function _execBridge( address token, uint256 amount, uint256 flags, @@ -491,7 +491,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { uint256 bridgeValue = ((flags & BRIDGE_VALUE_FLAG_BIT_MASK) != 0) ? amount + bridgeData.value : bridgeData.value; // Execute bridge call - _doCall(bridgeData.target, bridgeValue, _bridgeCallData); + _execCall(bridgeData.target, bridgeValue, _bridgeCallData); } // -------------------------------------- @@ -636,8 +636,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { mstore(add(0x60, ptr), address()) // calldata[68..99]: recipient = this contract (right-aligned, high 12 bytes are zero padding) mstore(add(0x4c, ptr), shl(0x60, user)) // calldata[48..67]: user address; trailing 12 zero bytes fill calldata[68..79] (recipient padding) // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which - // shifts the 20-byte address out of place and corrupts the calldata token. Same as - // 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. + // shifts the 20-byte address out of place and corrupts the calldata token. Same as 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. mstore(add(0x2c, ptr), shl(0x60, token)) // calldata[16..35]: token address; trailing 12 zero bytes fill calldata[36..47] (user padding) mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector at calldata[0..3]; 12 zero bytes fill calldata[4..15] (token padding); calldata begins at ptr+0x1c @@ -671,11 +670,11 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { if (useBalanceOf) { // Measure output as (balance after − balance before) at `outputReceiver` uint256 before = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver); - _doCallCalldata(swapData.target, swapData.value, swapCallData, false); + _execCallCalldata(swapData.target, swapData.value, swapCallData, false); finalAmount = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver) - before; } else { // Decode output from returndata - bytes memory ret = _doCallCalldata(swapData.target, swapData.value, swapCallData, true); + bytes memory ret = _execCallCalldata(swapData.target, swapData.value, swapCallData, true); finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); } } @@ -686,7 +685,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { * @param value Wei forwarded with the call. * @param data ABI-encoded calldata in memory. */ - function _doCall(address target, uint256 value, bytes memory data) internal { + function _execCall(address target, uint256 value, bytes memory data) internal { bool success; assembly ("memory-safe") { success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) @@ -715,7 +714,7 @@ contract OpenRouter is AccessControl, AllowanceHolderContext { * @param storeResult When true, copy returndata into memory even on success. * @return ret Returndata when `storeResult` is true or the call reverts (revert bubbles). */ - function _doCallCalldata(address target, uint256 value, bytes calldata data, bool storeResult) + function _execCallCalldata(address target, uint256 value, bytes calldata data, bool storeResult) internal returns (bytes memory ret) { diff --git a/src/common/lib/BytesSpliceLib.sol b/src/common/lib/BytesSpliceLib.sol index fc6a890..e094de6 100644 --- a/src/common/lib/BytesSpliceLib.sol +++ b/src/common/lib/BytesSpliceLib.sol @@ -2,8 +2,7 @@ pragma solidity 0.8.34; /// @title BytesSpliceLib -/// @notice Generalisation of the in-place calldata patching used in -/// GenericStakedRoute and BungeeApproveAndBridge. Supports patching +/// @notice Generalisation of the in-place calldata patching. Supports patching /// either a single 32-byte word (for `uint256` amount fields) or an /// arbitrary length copy from one bytes blob to another. library BytesSpliceLib { @@ -13,7 +12,6 @@ library BytesSpliceLib { error SplicePositionOutOfBounds(); /// @notice Overwrites a 32-byte word at `position` in `data` with `word`. - /// @dev Mirrors the GenericStakedRoute amount patching pattern. function spliceWord(bytes memory data, uint256 position, uint256 word) internal pure { // Bounds check: position + 32 must fit in data if (position + 32 > data.length) { From c1c4b4adbe0d4fa1168bacb3d7af992329ed05fe Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 25 May 2026 19:38:03 +0530 Subject: [PATCH 68/69] chore: prev. audit comments --- src/common/lib/CurrencyLib.sol | 1 + src/common/lib/RescueFundsLib.sol | 1 + src/common/utils/AccessControl.sol | 1 + src/common/utils/Ownable.sol | 1 + 4 files changed, 4 insertions(+) diff --git a/src/common/lib/CurrencyLib.sol b/src/common/lib/CurrencyLib.sol index 7208f66..6c7f208 100644 --- a/src/common/lib/CurrencyLib.sol +++ b/src/common/lib/CurrencyLib.sol @@ -5,6 +5,7 @@ import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; error TransferFailed(); +// @audit Audited before by Hexens: // @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf /// @title CurrencyLib /// @notice Token transfer + balance helpers that treat the canonical native /// pseudo-token (`0xEee...EEe`) the same way as the marketplace's diff --git a/src/common/lib/RescueFundsLib.sol b/src/common/lib/RescueFundsLib.sol index 221c215..22c4423 100644 --- a/src/common/lib/RescueFundsLib.sol +++ b/src/common/lib/RescueFundsLib.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.34; +// @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; error ZeroAddress(); diff --git a/src/common/utils/AccessControl.sol b/src/common/utils/AccessControl.sol index 3faca7d..51e3f72 100644 --- a/src/common/utils/AccessControl.sol +++ b/src/common/utils/AccessControl.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.34; import {Ownable} from "./Ownable.sol"; +// @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf abstract contract AccessControl is Ownable { mapping(bytes32 => mapping(address => bool)) private _permits; diff --git a/src/common/utils/Ownable.sol b/src/common/utils/Ownable.sol index dd83c0b..a7c7f17 100644 --- a/src/common/utils/Ownable.sol +++ b/src/common/utils/Ownable.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.34; +// @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf /// @title Ownable /// @notice Two-step ownership transfer, ported from /// marketplace/src/utils/Ownable.sol. Simpler than OpenZeppelin's From 220c7a7ee049f4a2ef6fe7913d4ee6b0ea7557c3 Mon Sep 17 00:00:00 2001 From: Sebastian T F Date: Mon, 25 May 2026 19:41:31 +0530 Subject: [PATCH 69/69] fix: prev. audit comment --- src/common/lib/CurrencyLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/lib/CurrencyLib.sol b/src/common/lib/CurrencyLib.sol index 6c7f208..56ca7e0 100644 --- a/src/common/lib/CurrencyLib.sol +++ b/src/common/lib/CurrencyLib.sol @@ -5,7 +5,7 @@ import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; error TransferFailed(); -// @audit Audited before by Hexens: // @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf +// @audit Audited before by Hexens: https://github.com/SocketDotTech/audits/blob/main/Bungee/12-2024%20-%20Bungee%20Protocol%20-%20Hexens.pdf /// @title CurrencyLib /// @notice Token transfer + balance helpers that treat the canonical native /// pseudo-token (`0xEee...EEe`) the same way as the marketplace's