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..e4f7021 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 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 ee7fad7..7b48de0 100644 --- a/src/debridge/DebridgeDlnHelper.sol +++ b/src/debridge/DebridgeDlnHelper.sol @@ -4,6 +4,8 @@ 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"; +import {IDlnDestination, Order} from "./interfaces/IDlnDestination.sol"; /// @title Debridge DLN Helper /// @notice helps simulate Debridge DLN message relaying with hooks @@ -12,45 +14,7 @@ contract DebridgeDlnHelper is Test { "CreatedOrder((uint64,bytes,uint256,bytes,uint256,uint256,bytes,uint256,bytes,bytes,bytes,bytes,bytes,bytes),bytes32,bytes,uint256,uint256,uint32,bytes)" ); - /// @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; - } + address constant TAKER_ADDRESS = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; struct HelpArgs { address dlnSource; @@ -206,11 +170,7 @@ contract DebridgeDlnHelper is Test { uint256 count = args.logs.length; for (uint256 i; i < count;) { - // https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/interacting-with-smart-contracts/placing-orders - // CreatedOrder is the event selector for the createOrder event emitted by the DeBridge DLN contract - if (args.logs[i].topics[0] == args.eventSelector) { - vm.selectFork(args.forkId); - + if (args.logs[i].emitter == args.dlnSource && args.logs[i].topics[0] == args.eventSelector) { ( Order memory order, bytes32 orderId, @@ -220,43 +180,48 @@ contract DebridgeDlnHelper is Test { uint32 reeferralCode, bytes memory metadata ) = abi.decode(args.logs[i].data, (Order, bytes32, bytes, uint256, uint256, uint32, bytes)); - DebridgeLogData memory logData = DebridgeLogData({ - order: order, - orderId: orderId, - affiliateFee: affiliateFee, - nativeFixFee: nativeFixFee, - percentFee: percentFee, - reeferralCode: reeferralCode, - metadata: metadata - }); 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; - // How do we fulfill the order? - // https://docs.debridge.finance/dln-the-debridge-liquidity-network-protocol/interacting-with-smart-contracts/filling-orders - - // deal tokens to receiver - address receiver = address(bytes20(logData.order.receiverDst)); - address token = address(bytes20(logData.order.takeTokenAddress)); - if (token == address(0)) { - deal(receiver, logData.order.takeAmount); - - if (logData.order.externalCall.length > 0) { - IExternalCallExecutor(receiver).onEtherReceived( - logData.orderId, receiver, logData.order.externalCall - ); - } + address dlnDestinationAddress = args.dlnDestination; + address takerAddress = TAKER_ADDRESS; + address unlockAuthority = takerAddress; + uint256 fulfillAmount = order.takeAmount; + address tokenAddress = address(bytes20(order.takeTokenAddress)); + 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 { - deal(token, receiver, logData.order.takeAmount); - - // execute hooks - if (logData.order.externalCall.length > 0) { - IExternalCallExecutor(receiver).onERC20Received( - logData.orderId, token, logData.order.takeAmount, receiver, logData.order.externalCall - ); - } + // 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); + IDlnDestination(dlnDestinationAddress).fulfillOrder{value: msgValue}( + order, fulfillAmount, orderId, permitEnvelope, unlockAuthority, takerAddress + ); + + vm.selectFork(vars.prevForkId); } } @@ -265,6 +230,8 @@ contract DebridgeDlnHelper is Test { } } - vm.selectFork(vars.prevForkId); + if (vm.activeFork() != vars.prevForkId) { + vm.selectFork(vars.prevForkId); + } } } 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/IDlnSource.sol b/src/debridge/interfaces/IDlnSource.sol index 0f8a6f1..705d7e1 100644 --- a/src/debridge/interfaces/IDlnSource.sol +++ b/src/debridge/interfaces/IDlnSource.sol @@ -44,7 +44,6 @@ interface IDlnSource { uint32 _referralCode, bytes calldata _permitEnvelope ) external payable returns (bytes32 orderId); - + function globalFixedNativeFee() external view returns (uint256); } - diff --git a/src/debridge/interfaces/IExternalCallExecutor.sol b/src/debridge/interfaces/IExternalCallExecutor.sol index 38572b2..dd3446c 100644 --- a/src/debridge/interfaces/IExternalCallExecutor.sol +++ b/src/debridge/interfaces/IExternalCallExecutor.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; + +pragma solidity ^0.8.0; interface IExternalCallExecutor { /** 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/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 e65bd82..6b3d3b1 100644 --- a/test/Debridge.t.sol +++ b/test/Debridge.t.sol @@ -5,15 +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"; contract DebridgeHelperTest is Test { DebridgeHelper debridgeHelper; - DebridgeDlnHelper debridgeDlnHelper; address public target = address(this); @@ -33,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; @@ -62,7 +55,6 @@ contract DebridgeHelperTest is Test { vm.selectFork(L1_FORK_ID); debridgeHelper = new DebridgeHelper(); - debridgeDlnHelper = new DebridgeDlnHelper(); allDstTargets.push(target); allDstTargets.push(target); @@ -93,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, @@ -137,33 +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; diff --git a/test/DebridgeDln.t.sol b/test/DebridgeDln.t.sol new file mode 100644 index 0000000..374f815 --- /dev/null +++ b/test/DebridgeDln.t.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.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"; + +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 counter); + // 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 /*nothing*/ ) + { + lastOrderId = _orderId; + lastFallbackAddress = _fallbackAddress; + lastPayload = _payload; + lastReceivedValue = msg.value; // Amount received by *this* contract from adapter + + counter++; + callSucceeded = true; + + emit Log(callSucceeded, msg.value, counter); + } + + /** + * @notice Increments counter if the expected amount matches _transferredAmount. + * @dev Expected amount is decoded from _payload. + */ + function onERC20Received( + bytes32 _orderId, + address _token, + uint256 _transferredAmount, // Amount received by *this* contract from adapter + address _fallbackAddress, + bytes memory _payload + ) external override returns (bool callSucceeded, bytes memory /*nothing*/ ) { + lastOrderId = _orderId; + lastToken = _token; + lastAmount = _transferredAmount; + lastFallbackAddress = _fallbackAddress; + lastPayload = _payload; + + counter++; + callSucceeded = true; + + emit Log(callSucceeded, _transferredAmount, counter); + } +} + +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; + 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 = 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 = 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"); + + // 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, 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); // Select L1 fork + debridgeDlnHelper = new DebridgeDlnHelper(); + 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 { + 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 + address makerAddress = MAKER_ADDRESS; // Use the defined constant + + // 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); + + // Use OrderCreation struct now + IDlnSource.OrderCreation memory orderCreation = IDlnSource.OrderCreation({ + giveTokenAddress: giveToken, + giveAmount: giveAmount, + takeTokenAddress: abi.encodePacked(takeToken), + takeAmount: takeAmount, + takeChainId: destinationChainId, + receiverDst: abi.encodePacked(target), // Receiver is the target address + givePatchAuthoritySrc: address(0), // No patch authority + orderAuthorityAddressDst: abi.encodePacked(address(0)), // No patch/cancel authority on dst + allowedTakerDst: "", // Allow any taker + externalCall: "", // No external call for this simple test + allowedCancelBeneficiarySrc: "" // Allow any cancel beneficiary + }); + + // 3. Simulate Order Creation on Source Chain (using Approve) + vm.recordLogs(); + // 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(); + + // 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); + + // 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); + + // 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 + } + + // ==================== 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: 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) + }); + + // 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: false, + requireSuccessfullExecution: true + }); + + // 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 + } +}