From 4bb65bccc47b8e54cb9d73616a525764e515e10f Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 17:40:33 +0100 Subject: [PATCH 1/7] fix: --- src/debridge/DebridgeDlnHelper.sol | 288 +++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 src/debridge/DebridgeDlnHelper.sol diff --git a/src/debridge/DebridgeDlnHelper.sol b/src/debridge/DebridgeDlnHelper.sol new file mode 100644 index 0000000..aad855f --- /dev/null +++ b/src/debridge/DebridgeDlnHelper.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// library imports +import "forge-std/Test.sol"; +import {IExternalCallExecutor} from "./interfaces/IExternalCallExecutor.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Debridge DLN Helper +/// @notice helps simulate Debridge DLN message relaying with hooks +contract DebridgeDlnHelper is Test { + bytes32 constant DlnOrderCreated = keccak256( + "CreatedOrder((uint64,bytes,uint256,bytes,uint256,uint256,bytes,uint256,bytes,bytes,bytes,bytes,bytes,bytes),bytes32,bytes,uint256,uint256,uint32,bytes)" + ); + + address constant TAKER_ADDRESS = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; + + /// @dev Struct representing an order. + struct Order { + /// Nonce for each maker. + uint64 makerOrderNonce; + /// Order maker address (EOA signer for EVM) in the source chain. + bytes makerSrc; + /// Chain ID where the order's was created. + uint256 giveChainId; + /// Address of the ERC-20 token that the maker is offering as part of this order. + /// Use the zero address to indicate that the maker is offering a native blockchain token (such as Ether, Matic, etc.). + bytes giveTokenAddress; + /// Amount of tokens the maker is offering. + uint256 giveAmount; + // the ID of the chain where an order should be fulfilled. + uint256 takeChainId; + /// Address of the ERC-20 token that the maker is willing to accept on the destination chain. + bytes takeTokenAddress; + /// Amount of tokens the maker is willing to accept on the destination chain. + uint256 takeAmount; + /// Address on the destination chain where funds should be sent upon order fulfillment. + bytes receiverDst; + /// Address on the source (current) chain authorized to patch the order by adding more input tokens, making it more attractive to takers. + bytes givePatchAuthoritySrc; + /// Address on the destination chain authorized to patch the order by reducing the take amount, making it more attractive to takers, + /// and can also cancel the order in the take chain. + bytes orderAuthorityAddressDst; + // An optional address restricting anyone in the open market from fulfilling + // this order but the given address. This can be useful if you are creating a order + // for a specific taker. By default, set to empty bytes array (0x) + bytes allowedTakerDst; + // An optional address on the source (current) chain where the given input tokens + // would be transferred to in case order cancellation is initiated by the orderAuthorityAddressDst + // on the destination chain. This property can be safely set to an empty bytes array (0x): + // in this case, tokens would be transferred to the arbitrary address specified + // by the orderAuthorityAddressDst upon order cancellation + bytes allowedCancelBeneficiarySrc; + /// An optional external call data payload. + bytes externalCall; + } + + struct HelpArgs { + address dlnSource; + address dlnDestination; + uint256 forkId; + uint256 destinationChainId; + bytes32 eventSelector; + Vm.Log[] logs; + } + + struct LocalVars { + uint256 prevForkId; + uint256 originChainId; + uint256 destinationChainId; + DebridgeLogData logData; + } + + struct DebridgeLogData { + Order order; + bytes32 orderId; + bytes affiliateFee; + uint256 nativeFixFee; + uint256 percentFee; + uint32 reeferralCode; + bytes metadata; + } + + interface IDlnDestination { + function fulfillOrder( + Order memory _order, + uint256 _fulFillAmount, + bytes32 _orderId, + bytes calldata _permitEnvelope, + address _unlockAuthority, + address _externalCallRewardBeneficiary + ) external payable; + } + + ////////////////////////////////////////////////////////////// + // EXTERNAL FUNCTIONS // + ////////////////////////////////////////////////////////////// + /// @notice helps process multiple destination messages to relay + /// @param dlnSource represents the source deBridge DLN + /// @param dlnDestinations represents the destination deBridge DLNs + /// @param forkIds represents the destination chain fork ids + /// @param destinationChainIds represents the destination chain ids + /// @param logs represents the recorded message logs + function help( + address dlnSource, + address[] memory dlnDestinations, + uint256[] memory forkIds, + uint256[] memory destinationChainIds, + Vm.Log[] calldata logs + ) external { + uint256 chains = destinationChainIds.length; + for (uint256 i; i < chains;) { + _help( + HelpArgs({ + dlnSource: dlnSource, + dlnDestination: dlnDestinations[i], + forkId: forkIds[i], + destinationChainId: destinationChainIds[i], + eventSelector: DlnOrderCreated, + logs: logs + }) + ); + unchecked { + ++i; + } + } + } + /// @notice helps process multiple destination messages to relay + /// @param dlnSource represents the source deBridge gate + /// @param dlnDestinations represents the destination deBridge gate + /// @param forkIds represents the destination chain fork ids + /// @param destinationChainIds represents the destination chain ids + /// @param eventSelector represents a custom event selector + /// @param logs represents the recorded message logs + + function help( + address dlnSource, + address[] memory dlnDestinations, + uint256[] memory forkIds, + uint256[] memory destinationChainIds, + bytes32 eventSelector, + Vm.Log[] calldata logs + ) external { + uint256 chains = destinationChainIds.length; + for (uint256 i; i < chains;) { + _help( + HelpArgs({ + dlnSource: dlnSource, + dlnDestination: dlnDestinations[i], + forkId: forkIds[i], + destinationChainId: destinationChainIds[i], + eventSelector: eventSelector, + logs: logs + }) + ); + unchecked { + ++i; + } + } + } + + /// @notice helps process single destination message to relay + /// @param dlnSource represents the source deBridge gate + /// @param dlnDestination represents the destination deBridge gate + /// @param forkId represents the destination chain fork id + /// @param destinationChainId represents the destination chain id + /// @param logs represents the recorded message logs + function help( + address dlnSource, + address dlnDestination, + uint256 forkId, + uint256 destinationChainId, + Vm.Log[] calldata logs + ) external { + _help( + HelpArgs({ + dlnSource: dlnSource, + dlnDestination: dlnDestination, + forkId: forkId, + destinationChainId: destinationChainId, + eventSelector: DlnOrderCreated, + logs: logs + }) + ); + } + + /// @notice helps process single destination message to relay + /// @param dlnSource represents the source deBridge gate + /// @param dlnDestination represents the destination deBridge gate + /// @param forkId represents the destination chain fork id + /// @param destinationChainId represents the destination chain id + /// @param eventSelector represents a custom event selector + /// @param logs represents the recorded message logs + function help( + address dlnSource, + address dlnDestination, + uint256 forkId, + uint256 destinationChainId, + bytes32 eventSelector, + Vm.Log[] calldata logs + ) external { + _help( + HelpArgs({ + dlnSource: dlnSource, + dlnDestination: dlnDestination, + forkId: forkId, + destinationChainId: destinationChainId, + eventSelector: eventSelector, + logs: logs + }) + ); + } + + /// @notice internal function to process a single destination message to relay + /// @param args represents the help arguments + function _help(HelpArgs memory args) internal { + LocalVars memory vars; + vars.originChainId = uint256(block.chainid); + vars.prevForkId = vm.activeFork(); + + uint256 count = args.logs.length; + for (uint256 i; i < count;) { + if (args.logs[i].emitter == args.dlnSource && args.logs[i].topics[0] == args.eventSelector) { + ( + Order memory order, + bytes32 orderId, + bytes memory affiliateFee, + uint256 nativeFixFee, + uint256 percentFee, + uint32 reeferralCode, + bytes memory metadata + ) = abi.decode(args.logs[i].data, (Order, bytes32, bytes, uint256, uint256, uint32, bytes)); + + if (order.takeChainId == args.destinationChainId) { + vm.selectFork(args.forkId); + + DebridgeLogData memory logData = DebridgeLogData({ + order: order, + orderId: orderId, + affiliateFee: affiliateFee, + nativeFixFee: nativeFixFee, + percentFee: percentFee, + reeferralCode: reeferralCode, + metadata: metadata + }); + vars.logData = logData; + + address dlnDestinationAddress = args.dlnDestination; + address takerAddress = TAKER_ADDRESS; + address unlockAuthority = takerAddress; + uint256 fulfillAmount = order.takeAmount; + address tokenAddress = address(bytes20(order.takeTokenAddress)); + bytes memory permitEnvelope = ""; + uint256 msgValue = 0; + + if (tokenAddress == address(0)) { + msgValue = fulfillAmount; + vm.deal(takerAddress, takerAddress.balance + msgValue); + } else { + vm.deal(tokenAddress, takerAddress, fulfillAmount); + vm.prank(takerAddress); + IERC20(tokenAddress).approve(dlnDestinationAddress, fulfillAmount); + } + + vm.prank(takerAddress, takerAddress); + IDlnDestination(dlnDestinationAddress).fulfillOrder{value: msgValue}( + order, + fulfillAmount, + orderId, + permitEnvelope, + unlockAuthority, + address(0) + ); + + vm.selectFork(vars.prevForkId); + } + } + + unchecked { + ++i; + } + } + + if (vm.activeFork() != vars.prevForkId) { + vm.selectFork(vars.prevForkId); + } + } +} From 1b7668194bb189e028382f92e438065b8aada188 Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 17:52:30 +0100 Subject: [PATCH 2/7] fix: changes --- .gitmodules | 3 + lib/openzeppelin-contracts | 1 + src/debridge/DebridgeDlnHelper.sol | 82 +++++------------ test/DebridgeDln.t.sol | 138 +++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 59 deletions(-) create mode 160000 lib/openzeppelin-contracts create mode 100644 test/DebridgeDln.t.sol diff --git a/.gitmodules b/.gitmodules index 0f07815..9337666 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..21c8312 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 21c8312b022f495ebe3621d5daeed20552b43ff9 diff --git a/src/debridge/DebridgeDlnHelper.sol b/src/debridge/DebridgeDlnHelper.sol index aad855f..ebdeba1 100644 --- a/src/debridge/DebridgeDlnHelper.sol +++ b/src/debridge/DebridgeDlnHelper.sol @@ -5,6 +5,9 @@ pragma solidity >=0.8.0; import "forge-std/Test.sol"; import {IExternalCallExecutor} from "./interfaces/IExternalCallExecutor.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {IDlnDestination, Order} from "./interfaces/IDlnDestination.sol"; /// @title Debridge DLN Helper /// @notice helps simulate Debridge DLN message relaying with hooks @@ -14,46 +17,10 @@ contract DebridgeDlnHelper is Test { ); address constant TAKER_ADDRESS = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; + uint256 constant TAKER_PRIVATE_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; - /// @dev Struct representing an order. - struct Order { - /// Nonce for each maker. - uint64 makerOrderNonce; - /// Order maker address (EOA signer for EVM) in the source chain. - bytes makerSrc; - /// Chain ID where the order's was created. - uint256 giveChainId; - /// Address of the ERC-20 token that the maker is offering as part of this order. - /// Use the zero address to indicate that the maker is offering a native blockchain token (such as Ether, Matic, etc.). - bytes giveTokenAddress; - /// Amount of tokens the maker is offering. - uint256 giveAmount; - // the ID of the chain where an order should be fulfilled. - uint256 takeChainId; - /// Address of the ERC-20 token that the maker is willing to accept on the destination chain. - bytes takeTokenAddress; - /// Amount of tokens the maker is willing to accept on the destination chain. - uint256 takeAmount; - /// Address on the destination chain where funds should be sent upon order fulfillment. - bytes receiverDst; - /// Address on the source (current) chain authorized to patch the order by adding more input tokens, making it more attractive to takers. - bytes givePatchAuthoritySrc; - /// Address on the destination chain authorized to patch the order by reducing the take amount, making it more attractive to takers, - /// and can also cancel the order in the take chain. - bytes orderAuthorityAddressDst; - // An optional address restricting anyone in the open market from fulfilling - // this order but the given address. This can be useful if you are creating a order - // for a specific taker. By default, set to empty bytes array (0x) - bytes allowedTakerDst; - // An optional address on the source (current) chain where the given input tokens - // would be transferred to in case order cancellation is initiated by the orderAuthorityAddressDst - // on the destination chain. This property can be safely set to an empty bytes array (0x): - // in this case, tokens would be transferred to the arbitrary address specified - // by the orderAuthorityAddressDst upon order cancellation - bytes allowedCancelBeneficiarySrc; - /// An optional external call data payload. - bytes externalCall; - } + bytes32 constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); struct HelpArgs { address dlnSource; @@ -81,17 +48,6 @@ contract DebridgeDlnHelper is Test { bytes metadata; } - interface IDlnDestination { - function fulfillOrder( - Order memory _order, - uint256 _fulFillAmount, - bytes32 _orderId, - bytes calldata _permitEnvelope, - address _unlockAuthority, - address _externalCallRewardBeneficiary - ) external payable; - } - ////////////////////////////////////////////////////////////// // EXTERNAL FUNCTIONS // ////////////////////////////////////////////////////////////// @@ -250,7 +206,7 @@ contract DebridgeDlnHelper is Test { address unlockAuthority = takerAddress; uint256 fulfillAmount = order.takeAmount; address tokenAddress = address(bytes20(order.takeTokenAddress)); - bytes memory permitEnvelope = ""; + bytes memory permitEnvelope; uint256 msgValue = 0; if (tokenAddress == address(0)) { @@ -258,18 +214,26 @@ contract DebridgeDlnHelper is Test { vm.deal(takerAddress, takerAddress.balance + msgValue); } else { vm.deal(tokenAddress, takerAddress, fulfillAmount); - vm.prank(takerAddress); - IERC20(tokenAddress).approve(dlnDestinationAddress, fulfillAmount); + bytes32 domainSeparator = IERC20Permit(tokenAddress).DOMAIN_SEPARATOR(); + uint256 nonce = IERC20Permit(tokenAddress).nonces(takerAddress); + uint256 deadline = block.timestamp + 1 hours; + + bytes32 permitStructHash = keccak256( + abi.encode( + PERMIT_TYPEHASH, takerAddress, dlnDestinationAddress, fulfillAmount, nonce, deadline + ) + ); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitStructHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(TAKER_PRIVATE_KEY, digest); + + permitEnvelope = abi.encodePacked(r, s, v); } vm.prank(takerAddress, takerAddress); IDlnDestination(dlnDestinationAddress).fulfillOrder{value: msgValue}( - order, - fulfillAmount, - orderId, - permitEnvelope, - unlockAuthority, - address(0) + order, fulfillAmount, orderId, permitEnvelope, unlockAuthority, address(0) ); vm.selectFork(vars.prevForkId); diff --git a/test/DebridgeDln.t.sol b/test/DebridgeDln.t.sol new file mode 100644 index 0000000..9abac29 --- /dev/null +++ b/test/DebridgeDln.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import "forge-std/Test.sol"; + +import {DebridgeDlnHelper} from "../src/debridge/DebridgeDlnHelper.sol"; +// Bring in the Order struct definition +import {DebridgeDlnHelper as DlnHelperContract} from "../src/debridge/DebridgeDlnHelper.sol"; + +// Placeholder interface for DlnSource - adjust if necessary +interface IDlnSource { + function createOrder(DlnHelperContract.Order calldata order) external payable; + // Add other relevant functions if needed +} + +// Minimal interface for DlnDestination - matching the one in DebridgeDlnHelper +interface IDlnDestination { + function fulfillOrder( + DlnHelperContract.Order memory _order, + uint256 _fulFillAmount, + bytes32 _orderId, + bytes calldata _permitEnvelope, + address _unlockAuthority, + address _externalCallRewardBeneficiary + ) external payable; +} + +contract DebridgeDlnHelperTest is Test { + DebridgeDlnHelper debridgeDlnHelper; + + address public target; // Receiver address for the test + + uint256 L1_FORK_ID; + uint256 POLYGON_FORK_ID; + uint256 ARBITRUM_FORK_ID; + + // --- Token Addresses --- + address constant L1_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + address constant POLYGON_USDC = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; // Native USDC on Polygon PoS + + // --- Chain IDs --- + uint256 constant L1_ID = 1; + uint256 constant ARBITRUM_ID = 42161; + uint256 constant POLYGON_ID = 137; + + // --- Debridge DLN Addresses (same on all chains based on user info) --- + address constant DLN_SOURCE_ADDRESS = 0xeF4fB24aD0916217251F553c0596F8Edc630EB66; + address constant DLN_DESTINATION_ADDRESS = 0x33B72F60F2CEB7BDb64873Ac10015a35bed81717; + + // --- RPC URLs --- + string RPC_ETH_MAINNET = vm.envString("ETH_MAINNET_RPC_URL"); + string RPC_ARBITRUM_MAINNET = vm.envString("ARBITRUM_MAINNET_RPC_URL"); + string RPC_POLYGON_MAINNET = vm.envString("POLYGON_MAINNET_RPC_URL"); + + // eth refund / receive funds + receive() external payable {} + + function setUp() external { + // Use block numbers known to have the contracts deployed and stable + // Note: Polygon block number might need adjustment if test fails + L1_FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET, 19000000); + ARBITRUM_FORK_ID = vm.createSelectFork(RPC_ARBITRUM_MAINNET, 180000000); + POLYGON_FORK_ID = vm.createSelectFork(RPC_POLYGON_MAINNET, 50000000); + + vm.selectFork(L1_FORK_ID); // Start on L1 fork + debridgeDlnHelper = new DebridgeDlnHelper(); + target = address(this); // Use the test contract itself as the receiver + } + + function testSingleDstDln_L1_to_Arbitrum_USDC() external { + vm.selectFork(L1_FORK_ID); + uint256 giveAmount = 100 * 1e6; // 100 USDC + uint256 takeAmount = 99 * 1e6; // Expect ~99 USDC on destination (slippage/fees simulated) + uint256 destinationChainId = ARBITRUM_ID; + address giveToken = L1_USDC; + address takeToken = ARBITRUM_USDC; // The token expected on the destination + + // 1. Prepare Order Struct + // Ensure the test contract (maker) has USDC + deal(giveToken, address(this), giveAmount); + // Approve the DLN Source contract + IERC20(giveToken).approve(DLN_SOURCE_ADDRESS, giveAmount); + + DlnHelperContract.Order memory order = DlnHelperContract.Order({ + makerOrderNonce: 0, // Example nonce + makerSrc: abi.encodePacked(address(this)), // Maker is this contract + giveChainId: L1_ID, + giveTokenAddress: abi.encodePacked(giveToken), + giveAmount: giveAmount, + takeChainId: destinationChainId, + takeTokenAddress: abi.encodePacked(takeToken), + takeAmount: takeAmount, + receiverDst: abi.encodePacked(target), // Receiver is the target address + givePatchAuthoritySrc: abi.encodePacked(address(0)), // No patch authority + orderAuthorityAddressDst: abi.encodePacked(address(0)), // No patch/cancel authority on dst + allowedTakerDst: "", // Allow any taker + allowedCancelBeneficiarySrc: "", // Allow any cancel beneficiary + externalCall: "" // No external call for this simple test + }); + + // 2. Simulate Order Creation on Source Chain + vm.recordLogs(); + // Call the actual DlnSource contract to emit the log the helper needs + // We assume a simple `createOrder` function exists. + // May require sending value if fees are involved (e.g., 0.1 ether) + // Adjust msg.value if needed based on actual contract requirements + uint256 fee = 0.01 ether; // Example fee, adjust if needed + vm.deal(address(this), address(this).balance + fee); // Ensure contract has ETH for fee + IDlnSource(DLN_SOURCE_ADDRESS).createOrder{value: fee}(order); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // 3. Use Helper to Process on Destination Chain + // The helper internally switches forks, simulates the taker, etc. + debridgeDlnHelper.help(DLN_SOURCE_ADDRESS, DLN_DESTINATION_ADDRESS, ARBITRUM_FORK_ID, ARBITRUM_ID, logs); + + // 4. Assert Final State on Destination Chain + vm.selectFork(ARBITRUM_FORK_ID); + uint256 expectedBalance = takeAmount; // For simulation, assume exact amount minus patches is received + uint256 actualBalance = IERC20(takeToken).balanceOf(target); + + // Use assertApproxEqAbs due to potential minor differences if fees/patches were complex + // Tolerance of 1 unit (e.g., 1 wei for USDC) + assertApproxEqAbs(actualBalance, expectedBalance, 1, "Final balance on destination mismatch"); + + // Optional: Check native balance if gas refunds are expected + // uint256 finalNativeBalance = target.balance; + // assertTrue(finalNativeBalance > initialNativeBalance, "Native balance did not increase"); + + vm.selectFork(L1_FORK_ID); // Switch back to L1 fork for cleanup/next test + } + + // TODO: Add testMultiDstDln similar to Debridge.t.sol + // TODO: Add tests for native asset transfers (ETH) + // TODO: Add tests involving externalCall hooks +} From 72e4a62a257604505e97bfb98d478f9335919400 Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 17:52:35 +0100 Subject: [PATCH 3/7] fix: --- lib/openzeppelin-contracts | 2 +- src/debridge/interfaces/IDlnDestination.sol | 53 +++++++++++++++++++ .../interfaces/IExternalCallExecutor.sol | 44 +++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/debridge/interfaces/IDlnDestination.sol create mode 100644 src/debridge/interfaces/IExternalCallExecutor.sol diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 21c8312..e4f7021 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 21c8312b022f495ebe3621d5daeed20552b43ff9 +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/src/debridge/interfaces/IDlnDestination.sol b/src/debridge/interfaces/IDlnDestination.sol new file mode 100644 index 0000000..bcf6f9c --- /dev/null +++ b/src/debridge/interfaces/IDlnDestination.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// @dev Struct representing an order. +struct Order { + /// Nonce for each maker. + uint64 makerOrderNonce; + /// Order maker address (EOA signer for EVM) in the source chain. + bytes makerSrc; + /// Chain ID where the order's was created. + uint256 giveChainId; + /// Address of the ERC-20 token that the maker is offering as part of this order. + /// Use the zero address to indicate that the maker is offering a native blockchain token (such as Ether, Matic, etc.). + bytes giveTokenAddress; + /// Amount of tokens the maker is offering. + uint256 giveAmount; + // the ID of the chain where an order should be fulfilled. + uint256 takeChainId; + /// Address of the ERC-20 token that the maker is willing to accept on the destination chain. + bytes takeTokenAddress; + /// Amount of tokens the maker is willing to accept on the destination chain. + uint256 takeAmount; + /// Address on the destination chain where funds should be sent upon order fulfillment. + bytes receiverDst; + /// Address on the source (current) chain authorized to patch the order by adding more input tokens, making it more attractive to takers. + bytes givePatchAuthoritySrc; + /// Address on the destination chain authorized to patch the order by reducing the take amount, making it more attractive to takers, + /// and can also cancel the order in the take chain. + bytes orderAuthorityAddressDst; + // An optional address restricting anyone in the open market from fulfilling + // this order but the given address. This can be useful if you are creating a order + // for a specific taker. By default, set to empty bytes array (0x) + bytes allowedTakerDst; + // An optional address on the source (current) chain where the given input tokens + // would be transferred to in case order cancellation is initiated by the orderAuthorityAddressDst + // on the destination chain. This property can be safely set to an empty bytes array (0x): + // in this case, tokens would be transferred to the arbitrary address specified + // by the orderAuthorityAddressDst upon order cancellation + bytes allowedCancelBeneficiarySrc; + /// An optional external call data payload. + bytes externalCall; +} + +interface IDlnDestination { + function fulfillOrder( + Order memory _order, + uint256 _fulFillAmount, + bytes32 _orderId, + bytes calldata _permitEnvelope, + address _unlockAuthority, + address _externalCallRewardBeneficiary + ) external payable; +} diff --git a/src/debridge/interfaces/IExternalCallExecutor.sol b/src/debridge/interfaces/IExternalCallExecutor.sol new file mode 100644 index 0000000..dd3446c --- /dev/null +++ b/src/debridge/interfaces/IExternalCallExecutor.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IExternalCallExecutor { + /** + * @notice Handles the receipt of Ether to the contract, then validates and executes a function call. + * @dev Only callable by the adapter. This function decodes the payload to extract execution data. + * If the function specified in the callData is prohibited, or the recipient contract is zero, + * all Ether is transferred to the fallback address. + * Otherwise, it attempts to execute the function call. Any remaining Ether is then transferred to the fallback address. + * @param _orderId The ID of the order that triggered this function. + * @param _fallbackAddress The address to receive any unspent Ether. + * @param _payload The encoded data containing the execution data. + * @return callSucceeded A boolean indicating whether the call was successful. + * @return callResult The data returned from the call. + */ + function onEtherReceived(bytes32 _orderId, address _fallbackAddress, bytes memory _payload) + external + payable + returns (bool callSucceeded, bytes memory callResult); + + /** + * @notice Handles the receipt of ERC20 tokens, validates and executes a function call. + * @dev Only callable by the adapter. This function decodes the payload to extract execution data. + * If the function specified in the callData is prohibited, or the recipient contract is zero, + * all received tokens are transferred to the fallback address. + * Otherwise, it attempts to execute the function call. Any remaining tokens are then transferred to the fallback address. + * @param _orderId The ID of the order that triggered this function. + * @param _token The address of the ERC20 token that was transferred. + * @param _transferredAmount The amount of tokens transferred. + * @param _fallbackAddress The address to receive any unspent tokens. + * @param _payload The encoded data containing the execution data. + * @return callSucceeded A boolean indicating whether the call was successful. + * @return callResult The data returned from the call. + */ + function onERC20Received( + bytes32 _orderId, + address _token, + uint256 _transferredAmount, + address _fallbackAddress, + bytes memory _payload + ) external returns (bool callSucceeded, bytes memory callResult); +} From ad5a3257b1b2862b7eb05695bb93f616e66f13bb Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 19:49:06 +0100 Subject: [PATCH 4/7] fix --- src/debridge/DebridgeDlnHelper.sol | 31 +- src/debridge/interfaces/IDlnSource.sol | 49 +++ src/debridge/libraries/DlnExternalCallLib.sol | 26 ++ test/Debridge.t.sol | 2 + test/DebridgeDln.t.sol | 312 +++++++++++++++--- test/Wormhole.AutomaticRelayer.t.sol | 2 +- 6 files changed, 347 insertions(+), 75 deletions(-) create mode 100644 src/debridge/interfaces/IDlnSource.sol create mode 100644 src/debridge/libraries/DlnExternalCallLib.sol diff --git a/src/debridge/DebridgeDlnHelper.sol b/src/debridge/DebridgeDlnHelper.sol index ebdeba1..c3c6bcd 100644 --- a/src/debridge/DebridgeDlnHelper.sol +++ b/src/debridge/DebridgeDlnHelper.sol @@ -5,8 +5,6 @@ pragma solidity >=0.8.0; import "forge-std/Test.sol"; import {IExternalCallExecutor} from "./interfaces/IExternalCallExecutor.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {IDlnDestination, Order} from "./interfaces/IDlnDestination.sol"; /// @title Debridge DLN Helper @@ -17,10 +15,6 @@ contract DebridgeDlnHelper is Test { ); address constant TAKER_ADDRESS = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; - uint256 constant TAKER_PRIVATE_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; - - bytes32 constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); struct HelpArgs { address dlnSource; @@ -206,29 +200,20 @@ contract DebridgeDlnHelper is Test { address unlockAuthority = takerAddress; uint256 fulfillAmount = order.takeAmount; address tokenAddress = address(bytes20(order.takeTokenAddress)); - bytes memory permitEnvelope; + bytes memory permitEnvelope = ""; // Always empty now uint256 msgValue = 0; if (tokenAddress == address(0)) { + // Native token transfer msgValue = fulfillAmount; vm.deal(takerAddress, takerAddress.balance + msgValue); } else { - vm.deal(tokenAddress, takerAddress, fulfillAmount); - bytes32 domainSeparator = IERC20Permit(tokenAddress).DOMAIN_SEPARATOR(); - uint256 nonce = IERC20Permit(tokenAddress).nonces(takerAddress); - uint256 deadline = block.timestamp + 1 hours; - - bytes32 permitStructHash = keccak256( - abi.encode( - PERMIT_TYPEHASH, takerAddress, dlnDestinationAddress, fulfillAmount, nonce, deadline - ) - ); - - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitStructHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(TAKER_PRIVATE_KEY, digest); - - permitEnvelope = abi.encodePacked(r, s, v); + // ERC20 token transfer - Use approve instead of permit + // Ensure taker has the tokens + deal(tokenAddress, takerAddress, fulfillAmount); + // Prank as taker to approve the DlnDestination contract + vm.prank(takerAddress); + IERC20(tokenAddress).approve(dlnDestinationAddress, fulfillAmount); } vm.prank(takerAddress, takerAddress); diff --git a/src/debridge/interfaces/IDlnSource.sol b/src/debridge/interfaces/IDlnSource.sol new file mode 100644 index 0000000..705d7e1 --- /dev/null +++ b/src/debridge/interfaces/IDlnSource.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IDlnSource { + struct OrderCreation { + // the address of the ERC-20 token you are giving; + // use the zero address to indicate you are giving a native blockchain token (ether, matic, etc). + address giveTokenAddress; + // the amount of tokens you are giving + uint256 giveAmount; + // the address of the ERC-20 token you are willing to take on the destination chain + bytes takeTokenAddress; + // the amount of tokens you are willing to take on the destination chain + uint256 takeAmount; + // the ID of the chain where an order should be fulfilled. + // Use the list of supported chains mentioned above + uint256 takeChainId; + // the address on the destination chain where the funds + // should be sent to upon order fulfillment + bytes receiverDst; + // the address on the source (current) chain who is allowed to patch the order + // giving more input tokens and thus making the order more attractive to takers, just in case + address givePatchAuthoritySrc; + // the address on the destination chain who is allowed to patch the order + // decreasing the take amount and thus making the order more attractive to takers, just in case + bytes orderAuthorityAddressDst; + // an optional address restricting anyone in the open market from fulfilling + // this order but the given address. This can be useful if you are creating a order + // for a specific taker. By default, set to empty bytes array (0x) + bytes allowedTakerDst; // *optional + // set to an empty bytes array (0x) + bytes externalCall; // N/A, *optional + // an optional address on the source (current) chain where the given input tokens + // would be transferred to in case order cancellation is initiated by the orderAuthorityAddressDst + // on the destination chain. This property can be safely set to an empty bytes array (0x): + // in this case, tokens would be transferred to the arbitrary address specified + // by the orderAuthorityAddressDst upon order cancellation + bytes allowedCancelBeneficiarySrc; // *optional + } + + function createOrder( + OrderCreation calldata _orderCreation, + bytes calldata _affiliateFee, + uint32 _referralCode, + bytes calldata _permitEnvelope + ) external payable returns (bytes32 orderId); + + function globalFixedNativeFee() external view returns (uint256); +} diff --git a/src/debridge/libraries/DlnExternalCallLib.sol b/src/debridge/libraries/DlnExternalCallLib.sol new file mode 100644 index 0000000..495a196 --- /dev/null +++ b/src/debridge/libraries/DlnExternalCallLib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +library DlnExternalCallLib { + struct ExternalCallEnvelopV1 { + // Address that will receive takeToken if ext call failed + address fallbackAddress; + // *optional. Smart contract that will execute ext call. + address executorAddress; + // fee that will pay for executor who will execute ext call + uint160 executionFee; + // If false, the taker must execute an external call with fulfill in a single transaction. + bool allowDelayedExecution; + // if true transaction that will execute ext call will fail if ext call is not success + bool requireSuccessfullExecution; + bytes payload; + } + + struct ExternalCallPayload { + // the address of the contract to call + address to; + // *optional. Tx gas for execute ext call + uint32 txGas; + bytes callData; + } +} diff --git a/test/Debridge.t.sol b/test/Debridge.t.sol index 87afa59..6b3d3b1 100644 --- a/test/Debridge.t.sol +++ b/test/Debridge.t.sol @@ -7,6 +7,8 @@ import "forge-std/Test.sol"; import {DebridgeHelper} from "src/debridge/DebridgeHelper.sol"; import {IDebridgeGate} from "src/debridge/interfaces/IDebridgeGate.sol"; +import "forge-std/console2.sol"; + contract DebridgeHelperTest is Test { DebridgeHelper debridgeHelper; diff --git a/test/DebridgeDln.t.sol b/test/DebridgeDln.t.sol index 9abac29..9f092ff 100644 --- a/test/DebridgeDln.t.sol +++ b/test/DebridgeDln.t.sol @@ -2,35 +2,87 @@ pragma solidity >=0.8.0; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "forge-std/Test.sol"; import {DebridgeDlnHelper} from "../src/debridge/DebridgeDlnHelper.sol"; // Bring in the Order struct definition import {DebridgeDlnHelper as DlnHelperContract} from "../src/debridge/DebridgeDlnHelper.sol"; -// Placeholder interface for DlnSource - adjust if necessary -interface IDlnSource { - function createOrder(DlnHelperContract.Order calldata order) external payable; - // Add other relevant functions if needed -} +import {IDlnSource} from "../src/debridge/interfaces/IDlnSource.sol"; +import {IExternalCallExecutor} from "../src/debridge/interfaces/IExternalCallExecutor.sol"; +import {DlnExternalCallLib} from "../src/debridge/libraries/DlnExternalCallLib.sol"; + +contract SampleExecutor is IExternalCallExecutor { + uint256 public counter; + address public lastToken; + uint256 public lastAmount; + bytes32 public lastOrderId; + address public lastFallbackAddress; + bytes public lastPayload; + uint256 public lastReceivedValue; + + event Log(bool callSucceeded, uint256 transferredAmount, uint256 expectedAmount); + // Allow receiving ETH + + receive() external payable {} + + /** + * @notice Increments counter if the expected amount matches msg.value. + * @dev Expected amount is decoded from _payload. + */ + function onEtherReceived(bytes32 _orderId, address _fallbackAddress, bytes memory _payload) + external + payable + override + returns (bool callSucceeded, bytes memory) + { + lastOrderId = _orderId; + lastFallbackAddress = _fallbackAddress; + lastPayload = _payload; + lastReceivedValue = msg.value; // Amount received by *this* contract from adapter + + // uint256 expectedAmount = abi.decode(_payload, (uint256)); + + counter++; + callSucceeded = true; + + emit Log(callSucceeded, msg.value, 0); + } -// Minimal interface for DlnDestination - matching the one in DebridgeDlnHelper -interface IDlnDestination { - function fulfillOrder( - DlnHelperContract.Order memory _order, - uint256 _fulFillAmount, + /** + * @notice Increments counter if the expected amount matches _transferredAmount. + * @dev Expected amount is decoded from _payload. + */ + function onERC20Received( bytes32 _orderId, - bytes calldata _permitEnvelope, - address _unlockAuthority, - address _externalCallRewardBeneficiary - ) external payable; + address _token, + uint256 _transferredAmount, // Amount received by *this* contract from adapter + address _fallbackAddress, + bytes memory _payload + ) external override returns (bool callSucceeded, bytes memory) { + lastOrderId = _orderId; + lastToken = _token; + lastAmount = _transferredAmount; + lastFallbackAddress = _fallbackAddress; + lastPayload = _payload; + + //uint256 expectedAmount = abi.decode(_payload, (uint256)); + + counter++; + callSucceeded = true; + + emit Log(callSucceeded, _transferredAmount, 0); + // callResult can be used to return data if needed + } } contract DebridgeDlnHelperTest is Test { DebridgeDlnHelper debridgeDlnHelper; address public target; // Receiver address for the test + // State variables for deployed executors on different forks + SampleExecutor executorArb; + // Add executorPoly if testing Polygon external calls uint256 L1_FORK_ID; uint256 POLYGON_FORK_ID; @@ -43,31 +95,40 @@ contract DebridgeDlnHelperTest is Test { // --- Chain IDs --- uint256 constant L1_ID = 1; - uint256 constant ARBITRUM_ID = 42161; + uint256 constant ARBITRUM_ID = 42_161; uint256 constant POLYGON_ID = 137; // --- Debridge DLN Addresses (same on all chains based on user info) --- address constant DLN_SOURCE_ADDRESS = 0xeF4fB24aD0916217251F553c0596F8Edc630EB66; - address constant DLN_DESTINATION_ADDRESS = 0x33B72F60F2CEB7BDb64873Ac10015a35bed81717; + address constant DLN_DESTINATION_ADDRESS = 0xE7351Fd770A37282b91D153Ee690B63579D6dd7f; // --- RPC URLs --- string RPC_ETH_MAINNET = vm.envString("ETH_MAINNET_RPC_URL"); string RPC_ARBITRUM_MAINNET = vm.envString("ARBITRUM_MAINNET_RPC_URL"); string RPC_POLYGON_MAINNET = vm.envString("POLYGON_MAINNET_RPC_URL"); - // eth refund / receive funds - receive() external payable {} + // Constants for the maker simulation + address constant MAKER_ADDRESS = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; // Using TAKER_ADDRESS from helper as + // maker function setUp() external { // Use block numbers known to have the contracts deployed and stable // Note: Polygon block number might need adjustment if test fails - L1_FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET, 19000000); - ARBITRUM_FORK_ID = vm.createSelectFork(RPC_ARBITRUM_MAINNET, 180000000); - POLYGON_FORK_ID = vm.createSelectFork(RPC_POLYGON_MAINNET, 50000000); + L1_FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET, 19_000_000); + ARBITRUM_FORK_ID = vm.createSelectFork(RPC_ARBITRUM_MAINNET, 180_000_000); + POLYGON_FORK_ID = vm.createSelectFork(RPC_POLYGON_MAINNET, 50_000_000); - vm.selectFork(L1_FORK_ID); // Start on L1 fork + vm.selectFork(L1_FORK_ID); // Select L1 fork debridgeDlnHelper = new DebridgeDlnHelper(); - target = address(this); // Use the test contract itself as the receiver + target = address(this); + vm.label(DLN_SOURCE_ADDRESS, "DLN_SOURCE_ADDRESS"); + vm.label(DLN_DESTINATION_ADDRESS, "DLN_DESTINATION_ADDRESS"); + + vm.selectFork(ARBITRUM_FORK_ID); // Select Arbitrum fork + executorArb = new SampleExecutor(); // Deploy on Arbitrum + vm.label(address(executorArb), "Executor_Arb"); + + vm.selectFork(L1_FORK_ID); // Return to L1 fork as default state for tests } function testSingleDstDln_L1_to_Arbitrum_USDC() external { @@ -77,46 +138,54 @@ contract DebridgeDlnHelperTest is Test { uint256 destinationChainId = ARBITRUM_ID; address giveToken = L1_USDC; address takeToken = ARBITRUM_USDC; // The token expected on the destination + address makerAddress = MAKER_ADDRESS; // Use the defined constant - // 1. Prepare Order Struct - // Ensure the test contract (maker) has USDC - deal(giveToken, address(this), giveAmount); - // Approve the DLN Source contract + // 1. Prepare OrderCreation Struct & Maker + // Ensure the maker address has USDC + deal(giveToken, makerAddress, giveAmount); + // Re-add approve call + vm.prank(makerAddress); // Prank as maker to approve IERC20(giveToken).approve(DLN_SOURCE_ADDRESS, giveAmount); - DlnHelperContract.Order memory order = DlnHelperContract.Order({ - makerOrderNonce: 0, // Example nonce - makerSrc: abi.encodePacked(address(this)), // Maker is this contract - giveChainId: L1_ID, - giveTokenAddress: abi.encodePacked(giveToken), + // Use OrderCreation struct now + IDlnSource.OrderCreation memory orderCreation = IDlnSource.OrderCreation({ + giveTokenAddress: giveToken, giveAmount: giveAmount, - takeChainId: destinationChainId, takeTokenAddress: abi.encodePacked(takeToken), takeAmount: takeAmount, + takeChainId: destinationChainId, receiverDst: abi.encodePacked(target), // Receiver is the target address - givePatchAuthoritySrc: abi.encodePacked(address(0)), // No patch authority + givePatchAuthoritySrc: address(0), // No patch authority orderAuthorityAddressDst: abi.encodePacked(address(0)), // No patch/cancel authority on dst allowedTakerDst: "", // Allow any taker - allowedCancelBeneficiarySrc: "", // Allow any cancel beneficiary - externalCall: "" // No external call for this simple test + externalCall: "", // No external call for this simple test + allowedCancelBeneficiarySrc: "" // Allow any cancel beneficiary }); - // 2. Simulate Order Creation on Source Chain + // 3. Simulate Order Creation on Source Chain (using Approve) vm.recordLogs(); - // Call the actual DlnSource contract to emit the log the helper needs - // We assume a simple `createOrder` function exists. - // May require sending value if fees are involved (e.g., 0.1 ether) - // Adjust msg.value if needed based on actual contract requirements - uint256 fee = 0.01 ether; // Example fee, adjust if needed - vm.deal(address(this), address(this).balance + fee); // Ensure contract has ETH for fee - IDlnSource(DLN_SOURCE_ADDRESS).createOrder{value: fee}(order); + // Fetch the required fixed fee from the contract + uint256 requiredFee = IDlnSource(DLN_SOURCE_ADDRESS).globalFixedNativeFee(); + + // The maker needs ETH for the fee + vm.deal(makerAddress, makerAddress.balance + requiredFee); + + // Prank as the maker to call createOrder + vm.prank(makerAddress); + // Pass the OrderCreation struct and the correct fee + // Pass empty bytes for permitEnvelope as we are using approve + IDlnSource(DLN_SOURCE_ADDRESS).createOrder{value: requiredFee}( + orderCreation, // The OrderCreation struct + "", // affiliateFee (bytes) + 0, // referralCode (uint32) + "" // permitEnvelope (empty) + ); Vm.Log[] memory logs = vm.getRecordedLogs(); - // 3. Use Helper to Process on Destination Chain - // The helper internally switches forks, simulates the taker, etc. + // 4. Use Helper to Process on Destination Chain (Helper handles the *destination* permit correctly) debridgeDlnHelper.help(DLN_SOURCE_ADDRESS, DLN_DESTINATION_ADDRESS, ARBITRUM_FORK_ID, ARBITRUM_ID, logs); - // 4. Assert Final State on Destination Chain + // 5. Assert Final State on Destination Chain vm.selectFork(ARBITRUM_FORK_ID); uint256 expectedBalance = takeAmount; // For simulation, assume exact amount minus patches is received uint256 actualBalance = IERC20(takeToken).balanceOf(target); @@ -132,7 +201,148 @@ contract DebridgeDlnHelperTest is Test { vm.selectFork(L1_FORK_ID); // Switch back to L1 fork for cleanup/next test } - // TODO: Add testMultiDstDln similar to Debridge.t.sol - // TODO: Add tests for native asset transfers (ETH) - // TODO: Add tests involving externalCall hooks + // ==================== External Call Tests ==================== // + + function testExternalCall_ERC20_L1_to_Arbitrum() external { + // --- Setup --- + vm.selectFork(L1_FORK_ID); + // Use the executor deployed on Arbitrum fork as the target + address targetExecutorAddress = address(executorArb); + + console.log("targetExecutorAddress", targetExecutorAddress); + + uint256 giveAmount = 100 * 1e6; // 100 L1 USDC + uint256 takeAmount = 99 * 1e6; // Expect 99 Arb USDC + uint256 destinationChainId = ARBITRUM_ID; + address giveToken = L1_USDC; + address takeToken = ARBITRUM_USDC; + address makerAddress = MAKER_ADDRESS; + + // --- Prepare Order --- + deal(giveToken, makerAddress, giveAmount); + vm.prank(makerAddress); + IERC20(giveToken).approve(DLN_SOURCE_ADDRESS, giveAmount); + + // 1. Create the inner payload for the SampleExecutor + bytes memory executorPayload = abi.encode(takeAmount); + + // 2. Create the Debridge External Call Envelope V1 + DlnExternalCallLib.ExternalCallEnvelopV1 memory dataEnvelope = DlnExternalCallLib.ExternalCallEnvelopV1({ + executorAddress: targetExecutorAddress, // Explicitly target our executor + executionFee: 0, + fallbackAddress: address(0), // No fallback needed for this test + payload: executorPayload, + allowDelayedExecution: true, // Allow fallback if needed, though test expects direct execution + requireSuccessfullExecution: false // Don't revert outer tx if executor fails (though we assert success + // later) + }); + + // 3. Prepend version byte (1) to the encoded envelope + bytes memory externalCall = abi.encodePacked(uint8(1), abi.encode(dataEnvelope)); + + // 4. Create the DLN Order pointing to the *Adapter's target executor* + // (which is our SampleExecutor instance on the Arb fork) + IDlnSource.OrderCreation memory orderCreation = IDlnSource.OrderCreation({ + giveTokenAddress: giveToken, + giveAmount: giveAmount, + takeTokenAddress: abi.encodePacked(takeToken), + takeAmount: takeAmount, + takeChainId: destinationChainId, + // receiverDst MUST be the executor address for the adapter to call it + receiverDst: abi.encodePacked(targetExecutorAddress), + givePatchAuthoritySrc: address(0), + orderAuthorityAddressDst: abi.encodePacked(address(0)), + allowedTakerDst: "", + externalCall: externalCall, // Use the correctly formatted envelope + allowedCancelBeneficiarySrc: "" + }); + + // --- Create Order --- + vm.recordLogs(); + uint256 requiredFee = IDlnSource(DLN_SOURCE_ADDRESS).globalFixedNativeFee(); + vm.deal(makerAddress, makerAddress.balance + requiredFee); + vm.prank(makerAddress); + IDlnSource(DLN_SOURCE_ADDRESS).createOrder{value: requiredFee}(orderCreation, "", 0, ""); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // --- Process with Helper --- + debridgeDlnHelper.help(DLN_SOURCE_ADDRESS, DLN_DESTINATION_ADDRESS, ARBITRUM_FORK_ID, ARBITRUM_ID, logs); + + // --- Assert Destination State --- + vm.selectFork(ARBITRUM_FORK_ID); + // Now the counter check should pass as the adapter calls the correct executor + assertEq(executorArb.counter(), 1, "Executor counter mismatch (ERC20)"); + // Optional checks on Arbitrum executor state: + assertEq(executorArb.lastToken(), takeToken, "Executor last token mismatch"); + assertEq(executorArb.lastAmount(), takeAmount, "Executor last amount mismatch"); + + vm.selectFork(L1_FORK_ID); // Revert fork + } + + function testExternalCall_Native_L1_to_Arbitrum() external { + // --- Setup --- + vm.selectFork(L1_FORK_ID); + // Use the executor deployed on Arbitrum fork as the target + address targetExecutorAddress = address(executorArb); + + uint256 giveAmount = 0.1 ether; + uint256 takeAmount = 0.099 ether; + uint256 destinationChainId = ARBITRUM_ID; + address giveToken = address(0); // Native ETH + address takeToken = address(0); // Native ETH on Arbitrum + address makerAddress = MAKER_ADDRESS; + + // --- Prepare Order --- + // 1. Create the inner payload for the SampleExecutor + bytes memory executorPayload = abi.encode(takeAmount); + + // 2. Create the Debridge External Call Envelope V1 + DlnExternalCallLib.ExternalCallEnvelopV1 memory dataEnvelope = DlnExternalCallLib.ExternalCallEnvelopV1({ + executorAddress: targetExecutorAddress, + executionFee: 0, + fallbackAddress: address(0), + payload: executorPayload, + allowDelayedExecution: true, + requireSuccessfullExecution: false + }); + + // 3. Prepend version byte (1) to the encoded envelope + bytes memory externalCall = abi.encodePacked(uint8(1), abi.encode(dataEnvelope)); + + // 4. Create the DLN Order + IDlnSource.OrderCreation memory orderCreation = IDlnSource.OrderCreation({ + giveTokenAddress: giveToken, + giveAmount: giveAmount, + takeTokenAddress: abi.encodePacked(takeToken), + takeAmount: takeAmount, + takeChainId: destinationChainId, + receiverDst: abi.encodePacked(targetExecutorAddress), // Target the executor + givePatchAuthoritySrc: address(0), + orderAuthorityAddressDst: abi.encodePacked(address(0)), + allowedTakerDst: "", + externalCall: externalCall, // Use the correctly formatted envelope + allowedCancelBeneficiarySrc: "" + }); + + // --- Create Order --- + vm.recordLogs(); + uint256 requiredFee = IDlnSource(DLN_SOURCE_ADDRESS).globalFixedNativeFee(); + vm.deal(makerAddress, makerAddress.balance + giveAmount + requiredFee); + vm.prank(makerAddress); + IDlnSource(DLN_SOURCE_ADDRESS).createOrder{value: giveAmount + requiredFee}(orderCreation, "", 0, ""); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // --- Process with Helper --- + debridgeDlnHelper.help(DLN_SOURCE_ADDRESS, DLN_DESTINATION_ADDRESS, ARBITRUM_FORK_ID, ARBITRUM_ID, logs); + + // --- Assert Destination State --- + vm.selectFork(ARBITRUM_FORK_ID); + // Now the counter check should pass + assertEq(executorArb.counter(), 1, "Executor counter mismatch (Native)"); + // Optional checks on Arbitrum executor state: + assertEq(executorArb.lastToken(), address(0), "Executor last token mismatch (Native)"); + assertEq(executorArb.lastReceivedValue(), takeAmount, "Executor last received value mismatch"); + + vm.selectFork(L1_FORK_ID); // Revert fork + } } diff --git a/test/Wormhole.AutomaticRelayer.t.sol b/test/Wormhole.AutomaticRelayer.t.sol index 5ae4e3e..e2388ef 100644 --- a/test/Wormhole.AutomaticRelayer.t.sol +++ b/test/Wormhole.AutomaticRelayer.t.sol @@ -357,6 +357,7 @@ contract WormholeAutomaticRelayerHelperTest is Test { messageKeys, 1 ); + vm.stopPrank(); wormholeHelper.helpWithCctpAndWormhole( L1_CHAIN_ID, @@ -366,7 +367,6 @@ contract WormholeAutomaticRelayerHelperTest is Test { 0xF3be9355363857F3e001be68856A2f96b4C39Ba9, vm.getRecordedLogs() ); - vm.stopPrank(); vm.selectFork(POLYGON_FORK_ID); assertEq(USDC_POLYGON.balanceOf(address(cctpTarget)), 100e6); From 4f4566e1f40f996b99affc7a8b13d87e172bb38f Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 19:50:43 +0100 Subject: [PATCH 5/7] fix: formatting --- src/across/AcrossV3Helper.sol | 10 +++++----- src/debridge/DebridgeDlnHelper.sol | 1 - src/debridge/interfaces/IDlnSource.sol | 1 - src/debridge/interfaces/IExternalCallExecutor.sol | 1 - src/layerzero/LayerZeroHelper.sol | 5 ++--- src/wormhole/automatic-relayer/WormholeHelper.sol | 1 - test/Across.t.sol | 10 +++++++--- test/Debridge.t.sol | 1 - 8 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/across/AcrossV3Helper.sol b/src/across/AcrossV3Helper.sol index 629bce2..d57fd3d 100644 --- a/src/across/AcrossV3Helper.sol +++ b/src/across/AcrossV3Helper.sol @@ -6,14 +6,13 @@ import "forge-std/Test.sol"; import {IAcrossSpokePoolV3} from "./interfaces/IAcrossSpokePoolV3.sol"; import {IERC20} from "./interfaces/IERC20.sol"; - /// @title AcrossV3 Helper /// @notice helps simulate AcrossV3 message relaying contract AcrossV3Helper is Test { bytes32 constant V3FundsDeposited = keccak256( "V3FundsDeposited(address,address,uint256,uint256,uint256,uint32,uint32,uint32,uint32,address,address,address,bytes)" ); - + bytes32 constant FundsDeposited = keccak256( "FundsDeposited(bytes32,bytes32,uint256,uint256,uint256,uint256,uint32,uint32,uint32,bytes32,bytes32,bytes32,bytes)" ); @@ -136,11 +135,12 @@ contract AcrossV3Helper is Test { // V3FundsDeposited is the event selector for the V3FundsDeposited event emitted by the SpokePool contract // Relayers should note that all deposits in V3 are associated with V3FundsDeposited events // and must be filled using the fillV3Relay function of the SpokePool contract. - if ( (args.logs[i].topics[0] == FundsDeposited || args.logs[i].topics[0] == V3FundsDeposited) && args.logs[i].emitter == args.sourceSpokePool) { + if ( + (args.logs[i].topics[0] == FundsDeposited || args.logs[i].topics[0] == V3FundsDeposited) + && args.logs[i].emitter == args.sourceSpokePool + ) { vars.destinationChainId = uint256(args.logs[i].topics[1]); - - if (vars.destinationChainId == args.destinationChainId) { vars.logData = _decodeLogData(args.logs[i]); diff --git a/src/debridge/DebridgeDlnHelper.sol b/src/debridge/DebridgeDlnHelper.sol index cd44e69..c3c6bcd 100644 --- a/src/debridge/DebridgeDlnHelper.sol +++ b/src/debridge/DebridgeDlnHelper.sol @@ -16,7 +16,6 @@ contract DebridgeDlnHelper is Test { address constant TAKER_ADDRESS = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; - struct HelpArgs { address dlnSource; address dlnDestination; diff --git a/src/debridge/interfaces/IDlnSource.sol b/src/debridge/interfaces/IDlnSource.sol index 3cae418..705d7e1 100644 --- a/src/debridge/interfaces/IDlnSource.sol +++ b/src/debridge/interfaces/IDlnSource.sol @@ -47,4 +47,3 @@ interface IDlnSource { function globalFixedNativeFee() external view returns (uint256); } - diff --git a/src/debridge/interfaces/IExternalCallExecutor.sol b/src/debridge/interfaces/IExternalCallExecutor.sol index 59850d1..dd3446c 100644 --- a/src/debridge/interfaces/IExternalCallExecutor.sol +++ b/src/debridge/interfaces/IExternalCallExecutor.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; - interface IExternalCallExecutor { /** * @notice Handles the receipt of Ether to the contract, then validates and executes a function call. diff --git a/src/layerzero/LayerZeroHelper.sol b/src/layerzero/LayerZeroHelper.sol index dd81666..23c3754 100644 --- a/src/layerzero/LayerZeroHelper.sol +++ b/src/layerzero/LayerZeroHelper.sol @@ -196,9 +196,8 @@ contract LayerZeroHelper is Test { Vm.Log memory log = logs[i]; // unsure if the default library always emits the event if ( - log - /*log.emitter == defaultLibrary &&*/ - .topics[0] == eventSelector + /*log.emitter == defaultLibrary &&*/ + log.topics[0] == eventSelector ) { bytes memory payload = abi.decode(log.data, (bytes)); LayerZeroPacket.Packet memory packet = LayerZeroPacket.getPacket(payload); diff --git a/src/wormhole/automatic-relayer/WormholeHelper.sol b/src/wormhole/automatic-relayer/WormholeHelper.sol index f700e4f..6951686 100644 --- a/src/wormhole/automatic-relayer/WormholeHelper.sol +++ b/src/wormhole/automatic-relayer/WormholeHelper.sol @@ -352,7 +352,6 @@ contract WormholeHelper is Test { v.additionalVAAs = new bytes[](v.indicesCache.length - 1); v.currIndex; - if (v.indicesCache.length > 1 && expDstAddress != address(0)) { for (uint256 j; j < v.indicesCache.length; j++) { log = logs[v.indicesCache[j]]; diff --git a/test/Across.t.sol b/test/Across.t.sol index bc786da..b0f7aeb 100644 --- a/test/Across.t.sol +++ b/test/Across.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0; import "forge-std/Test.sol"; -import { console2 } from "forge-std/console2.sol"; +import {console2} from "forge-std/console2.sol"; import {AcrossV3Helper} from "src/across/AcrossV3Helper.sol"; import {IAcrossSpokePoolV3} from "src/across/interfaces/IAcrossSpokePoolV3.sol"; @@ -134,7 +134,9 @@ contract AcrossV3HelperTest is Test { refundChainIds[0] = L1_ID; refundChainIds[1] = L1_ID; - acrossV3Helper.help(L1_spokePool, allDstSpokePools, RELAYER, 0, allDstForks, allDstChainIds, refundChainIds, logs); + acrossV3Helper.help( + L1_spokePool, allDstSpokePools, RELAYER, 0, allDstForks, allDstChainIds, refundChainIds, logs + ); vm.selectFork(POLYGON_FORK_ID); assertEq(target.amount(), 12); @@ -187,7 +189,9 @@ contract AcrossV3HelperTest is Test { vm.recordLogs(); _someCrossChainFunctionInYourContract(L1_spokePool, POLYGON_ID); Vm.Log[] memory logs = vm.getRecordedLogs(); - acrossV3Helper.help(L1_spokePool, POLYGON_spokePool, RELAYER, 1736349707, POLYGON_FORK_ID, POLYGON_ID, L1_ID, logs); + acrossV3Helper.help( + L1_spokePool, POLYGON_spokePool, RELAYER, 1736349707, POLYGON_FORK_ID, POLYGON_ID, L1_ID, logs + ); vm.selectFork(POLYGON_FORK_ID); assertEq(target.amount(), 12); diff --git a/test/Debridge.t.sol b/test/Debridge.t.sol index a6f79c7..f9ece56 100644 --- a/test/Debridge.t.sol +++ b/test/Debridge.t.sol @@ -163,7 +163,6 @@ contract DebridgeHelperTest is Test { 0, "" ); - } function testMultiDstDebridge() external { From a154ab6c60241e8f6024a0b9bef62cd3329d81cc Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 19:51:34 +0100 Subject: [PATCH 6/7] fix --- test/Debridge.t.sol | 60 --------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/test/Debridge.t.sol b/test/Debridge.t.sol index f9ece56..6b3d3b1 100644 --- a/test/Debridge.t.sol +++ b/test/Debridge.t.sol @@ -5,17 +5,12 @@ import {IERC20} from "src/across/interfaces/IERC20.sol"; import "forge-std/Test.sol"; import {DebridgeHelper} from "src/debridge/DebridgeHelper.sol"; -import {DebridgeDlnHelper} from "src/debridge/DebridgeDlnHelper.sol"; import {IDebridgeGate} from "src/debridge/interfaces/IDebridgeGate.sol"; -import {IDlnSource} from "src/debridge/interfaces/IDlnSource.sol"; - -import "forge-std/console2.sol"; import "forge-std/console2.sol"; contract DebridgeHelperTest is Test { DebridgeHelper debridgeHelper; - DebridgeDlnHelper debridgeDlnHelper; address public target = address(this); @@ -35,10 +30,6 @@ contract DebridgeHelperTest is Test { address constant ARBITRUM_debridge = 0x43dE2d77BF8027e25dBD179B491e8d64f38398aA; address constant POLYGON_debridge = 0x43dE2d77BF8027e25dBD179B491e8d64f38398aA; - address constant L1_DEBRIDGE_DLN = 0xeF4fB24aD0916217251F553c0596F8Edc630EB66; - address constant ARBITRUM_DEBRIDGE_DLN = 0xeF4fB24aD0916217251F553c0596F8Edc630EB66; - address constant POLYGON_DEBRIDGE_DLN = 0xeF4fB24aD0916217251F553c0596F8Edc630EB66; - address[] public allDstTargets; uint256[] public allDstChainIds; uint256[] public allDstForks; @@ -64,7 +55,6 @@ contract DebridgeHelperTest is Test { vm.selectFork(L1_FORK_ID); debridgeHelper = new DebridgeHelper(); - debridgeDlnHelper = new DebridgeDlnHelper(); allDstTargets.push(target); allDstTargets.push(target); @@ -95,30 +85,6 @@ contract DebridgeHelperTest is Test { assertApproxEqAbs(IERC20(ARBITRUM_DEBRIDGE_TOKEN).balanceOf(target), amount, amount * 1e4 / 1e5); } - function testSimpleDebridgeDln() external { - vm.selectFork(L1_FORK_ID); - uint256 amount = 1e10; - - console2.log("----A"); - // || - // || - // \/ This is the part of the code you could copy to use the DebridgeHelper - // in your own tests. - vm.recordLogs(); - _someCrossChainFunctionInYourContractDln(L1_DEBRIDGE_DLN, amount); - Vm.Log[] memory logs = vm.getRecordedLogs(); - console2.log("----B"); - - debridgeDlnHelper.help(L1_DEBRIDGE_DLN, ARBITRUM_DEBRIDGE_DLN, ARBITRUM_FORK_ID, ARBITRUM_ID, logs); - console2.log("----C"); - // /\ - // || - // || - - vm.selectFork(ARBITRUM_FORK_ID); - assertApproxEqAbs(IERC20(ARBITRUM_USDC).balanceOf(address(this)), amount, amount * 1e4 / 1e5); - } - function _someCrossChainFunctionInYourContract( address sourceDebridgeGate, uint256 destinationChainId, @@ -139,32 +105,6 @@ contract DebridgeHelperTest is Test { ); } - function _someCrossChainFunctionInYourContractDln(address sourceDebridgeDln, uint256 amount) internal { - uint256 nativeFixFee = IDebridgeGate(sourceDebridgeDln).globalFixedNativeFee(); - - deal(L1_USDC, address(this), amount); - IERC20(L1_USDC).approve(sourceDebridgeDln, amount); - - IDlnSource(sourceDebridgeDln).createOrder{value: nativeFixFee}( - IDlnSource.OrderCreation({ - giveTokenAddress: L1_USDC, - giveAmount: amount, - takeTokenAddress: abi.encodePacked(ARBITRUM_USDC), - takeAmount: amount - amount * 1e4 / 1e5, - takeChainId: ARBITRUM_ID, - receiverDst: abi.encodePacked(address(this)), - givePatchAuthoritySrc: address(this), - orderAuthorityAddressDst: abi.encodePacked(address(this)), - allowedTakerDst: "", - externalCall: "", - allowedCancelBeneficiarySrc: "" - }), - "", - 0, - "" - ); - } - function testMultiDstDebridge() external { vm.selectFork(L1_FORK_ID); uint256 amount = 1e10; From 5f5308817a103f72d0f3217211dfcb4874921205 Mon Sep 17 00:00:00 2001 From: 0xTimepunk Date: Fri, 18 Apr 2025 20:09:59 +0100 Subject: [PATCH 7/7] fix --- src/debridge/DebridgeDlnHelper.sol | 2 +- test/DebridgeDln.t.sol | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/debridge/DebridgeDlnHelper.sol b/src/debridge/DebridgeDlnHelper.sol index c3c6bcd..7b48de0 100644 --- a/src/debridge/DebridgeDlnHelper.sol +++ b/src/debridge/DebridgeDlnHelper.sol @@ -218,7 +218,7 @@ contract DebridgeDlnHelper is Test { vm.prank(takerAddress, takerAddress); IDlnDestination(dlnDestinationAddress).fulfillOrder{value: msgValue}( - order, fulfillAmount, orderId, permitEnvelope, unlockAuthority, address(0) + order, fulfillAmount, orderId, permitEnvelope, unlockAuthority, takerAddress ); vm.selectFork(vars.prevForkId); diff --git a/test/DebridgeDln.t.sol b/test/DebridgeDln.t.sol index 9f092ff..374f815 100644 --- a/test/DebridgeDln.t.sol +++ b/test/DebridgeDln.t.sol @@ -21,7 +21,7 @@ contract SampleExecutor is IExternalCallExecutor { bytes public lastPayload; uint256 public lastReceivedValue; - event Log(bool callSucceeded, uint256 transferredAmount, uint256 expectedAmount); + event Log(bool callSucceeded, uint256 transferredAmount, uint256 counter); // Allow receiving ETH receive() external payable {} @@ -34,19 +34,17 @@ contract SampleExecutor is IExternalCallExecutor { external payable override - returns (bool callSucceeded, bytes memory) + returns (bool callSucceeded, bytes memory /*nothing*/ ) { lastOrderId = _orderId; lastFallbackAddress = _fallbackAddress; lastPayload = _payload; lastReceivedValue = msg.value; // Amount received by *this* contract from adapter - // uint256 expectedAmount = abi.decode(_payload, (uint256)); - counter++; callSucceeded = true; - emit Log(callSucceeded, msg.value, 0); + emit Log(callSucceeded, msg.value, counter); } /** @@ -59,20 +57,17 @@ contract SampleExecutor is IExternalCallExecutor { uint256 _transferredAmount, // Amount received by *this* contract from adapter address _fallbackAddress, bytes memory _payload - ) external override returns (bool callSucceeded, bytes memory) { + ) external override returns (bool callSucceeded, bytes memory /*nothing*/ ) { lastOrderId = _orderId; lastToken = _token; lastAmount = _transferredAmount; lastFallbackAddress = _fallbackAddress; lastPayload = _payload; - //uint256 expectedAmount = abi.decode(_payload, (uint256)); - counter++; callSucceeded = true; - emit Log(callSucceeded, _transferredAmount, 0); - // callResult can be used to return data if needed + emit Log(callSucceeded, _transferredAmount, counter); } } @@ -232,8 +227,8 @@ contract DebridgeDlnHelperTest is Test { executionFee: 0, fallbackAddress: address(0), // No fallback needed for this test payload: executorPayload, - allowDelayedExecution: true, // Allow fallback if needed, though test expects direct execution - requireSuccessfullExecution: false // Don't revert outer tx if executor fails (though we assert success + allowDelayedExecution: false, // Allow fallback if needed, though test expects direct execution + requireSuccessfullExecution: true // Don't revert outer tx if executor fails (though we assert success // later) }); @@ -302,8 +297,8 @@ contract DebridgeDlnHelperTest is Test { executionFee: 0, fallbackAddress: address(0), payload: executorPayload, - allowDelayedExecution: true, - requireSuccessfullExecution: false + allowDelayedExecution: false, + requireSuccessfullExecution: true }); // 3. Prepend version byte (1) to the encoded envelope