diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index f64c822a6..10d55fb38 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -36,7 +36,7 @@ jobs: run: forge test - name: Coverage working-directory: contracts - run: forge coverage --exclude-tests --report=lcov + run: forge coverage --ir-minimum --exclude-tests --report=lcov - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v5 with: diff --git a/contracts/foundry.toml b/contracts/foundry.toml index c03b2a570..639f3f434 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -2,7 +2,7 @@ solc_version = "0.8.33" optimizer = true optimizer_runs = 200 -via_ir = false +via_ir = true test = 'test' script = 'scripts' fs_permissions = [ @@ -21,6 +21,9 @@ ignored_error_codes = [ no_match_test = "testRegenerate*" +# Coverage: use --ir-minimum to avoid "stack too deep" when running forge coverage +# Example: forge coverage --ir-minimum --exclude-tests --report=lcov + # Production profile: via_ir enabled for deployment contracts [profile.production] via_ir = true diff --git a/contracts/scripts/l2-integration/across/deploy/DeploySnowbridgeL1Adaptor.s.sol b/contracts/scripts/l2-integration/across/deploy/DeploySnowbridgeL1Adaptor.s.sol index 33a2d5cb4..f46b18e39 100644 --- a/contracts/scripts/l2-integration/across/deploy/DeploySnowbridgeL1Adaptor.s.sol +++ b/contracts/scripts/l2-integration/across/deploy/DeploySnowbridgeL1Adaptor.s.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.33; import {Script, console} from "forge-std/Script.sol"; -import {SPOKE_POOL as SEPOLIA_SPOKE_POOL, WETH9 as SEPOLIA_WETH9} from "../constants/Sepolia.sol"; -import {SPOKE_POOL as MAINNET_SPOKE_POOL, WETH9 as MAINNET_WETH9} from "../constants/Mainnet.sol"; +import {SPOKE_POOL as SEPOLIA_SPOKE_POOL, WETH9 as SEPOLIA_WETH9, GATEWAY as SEPOLIA_GATEWAY} from "../constants/Sepolia.sol"; +import {SPOKE_POOL as MAINNET_SPOKE_POOL, WETH9 as MAINNET_WETH9, GATEWAY as MAINNET_GATEWAY} from "../constants/Mainnet.sol"; import {SnowbridgeL1Adaptor} from "../../../../src/l2-integration/SnowbridgeL1Adaptor.sol"; contract DeploySnowbridgeL1Adaptor is Script { @@ -16,18 +16,21 @@ contract DeploySnowbridgeL1Adaptor is Script { address SPOKE_POOL_ADDRESS; address BASE_MULTI_CALL_HANDLER_ADDRESS; address WETH9_ADDRESS; + address GATEWAY_ADDRESS; if (keccak256(bytes(vm.envString("L1_NETWORK"))) == keccak256(bytes("mainnet"))) { SPOKE_POOL_ADDRESS = MAINNET_SPOKE_POOL; WETH9_ADDRESS = MAINNET_WETH9; + GATEWAY_ADDRESS = MAINNET_GATEWAY; } else if (keccak256(bytes(vm.envString("L1_NETWORK"))) == keccak256(bytes("sepolia"))) { SPOKE_POOL_ADDRESS = SEPOLIA_SPOKE_POOL; WETH9_ADDRESS = SEPOLIA_WETH9; + GATEWAY_ADDRESS = SEPOLIA_GATEWAY; } else { revert("Unsupported L1 network"); } - snowbridgeL1Adaptor = new SnowbridgeL1Adaptor(SPOKE_POOL_ADDRESS, WETH9_ADDRESS); + snowbridgeL1Adaptor = new SnowbridgeL1Adaptor(SPOKE_POOL_ADDRESS, WETH9_ADDRESS, GATEWAY_ADDRESS); console.log("Snowbridge L1 Adaptor deployed at:", address(snowbridgeL1Adaptor)); return; } diff --git a/contracts/src/l2-integration/SnowbridgeL1Adaptor.sol b/contracts/src/l2-integration/SnowbridgeL1Adaptor.sol index 650c05da9..8ca4ebc88 100644 --- a/contracts/src/l2-integration/SnowbridgeL1Adaptor.sol +++ b/contracts/src/l2-integration/SnowbridgeL1Adaptor.sol @@ -6,11 +6,13 @@ import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; import {WETH9} from "canonical-weth/WETH9.sol"; import {ISpokePool, IMessageHandler} from "./interfaces/ISpokePool.sol"; import {DepositParams, Instructions, Call} from "./Types.sol"; +import {Agent} from "../Agent.sol"; contract SnowbridgeL1Adaptor { using SafeERC20 for IERC20; ISpokePool public immutable SPOKE_POOL; WETH9 public immutable L1_WETH9; + address public immutable GATEWAY; /************************************** * EVENTS * @@ -19,15 +21,27 @@ contract SnowbridgeL1Adaptor { event DepositCallInvoked(bytes32 topic, uint256 depositId); event DepositCallFailed(bytes32 topic); - constructor(address _spokePool, address _l1weth9) { + constructor(address _spokePool, address _l1weth9, address _gateway) { SPOKE_POOL = ISpokePool(_spokePool); L1_WETH9 = WETH9(payable(_l1weth9)); + GATEWAY = _gateway; + } + + modifier onlyRegisteredGatewayAgent() { + require( + Agent(payable(msg.sender)).GATEWAY() == GATEWAY, + "Caller is not a registered Gateway Agent" + ); + _; } // Send ERC20 token on L1 to L2, the fee (params.inputAmount - params.outputAmount) should be calculated off-chain // following https://docs.across.to/reference/api-reference#get-swap-approval // The call requires pre-funding of the contract with the input tokens. - function depositToken(DepositParams calldata params, address recipient, bytes32 topic) public { + function depositToken(DepositParams calldata params, address recipient, bytes32 topic) + public + onlyRegisteredGatewayAgent + { require(params.inputToken != address(0), "Input token cannot be zero address"); checkInputs(params, recipient); IERC20(params.inputToken).forceApprove(address(SPOKE_POOL), params.inputAmount); @@ -54,6 +68,7 @@ contract SnowbridgeL1Adaptor { // The call requires pre-funding of the contract with native Ether. function depositNativeEther(DepositParams calldata params, address recipient, bytes32 topic) public + onlyRegisteredGatewayAgent { require( params.inputToken == address(0), diff --git a/contracts/src/l2-integration/SnowbridgeL2Adaptor.sol b/contracts/src/l2-integration/SnowbridgeL2Adaptor.sol index 43bf7d113..9bd446e80 100644 --- a/contracts/src/l2-integration/SnowbridgeL2Adaptor.sol +++ b/contracts/src/l2-integration/SnowbridgeL2Adaptor.sol @@ -88,6 +88,7 @@ contract SnowbridgeL2Adaptor { ); L2_WETH9.deposit{value: params.inputAmount}(); } else { + require(msg.value == 0, "Sent value must be zero for WETH deposits"); // Deposit WETH IERC20(address(L2_WETH9)) .safeTransferFrom(msg.sender, address(this), params.inputAmount); diff --git a/contracts/test/GatewayV2SnowbridgeL2.t.sol b/contracts/test/GatewayV2SnowbridgeL2.t.sol new file mode 100644 index 000000000..cc8b9139c --- /dev/null +++ b/contracts/test/GatewayV2SnowbridgeL2.t.sol @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.33; + +// Standalone unit tests for Agent -> SnowbridgeL1Adaptor flow (no GatewayV2Test inheritance +// to avoid stack-too-deep when compiling without via_ir). + +import {Test} from "forge-std/Test.sol"; +import {IGatewayV2} from "../src/v2/IGateway.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {MockGateway} from "./mocks/MockGateway.sol"; +import {AgentExecutor} from "../src/AgentExecutor.sol"; +import {Initializer} from "../src/Initializer.sol"; +import {Constants} from "../src/Constants.sol"; +import {SetOperatingModeParams} from "../src/v2/Types.sol"; +import {OperatingMode} from "../src/Types.sol"; +import {Verification} from "../src/Verification.sol"; +import {WETH9} from "canonical-weth/WETH9.sol"; +import {UD60x18, ud60x18} from "prb/math/src/UD60x18.sol"; +import {SnowbridgeL1Adaptor} from "../src/l2-integration/SnowbridgeL1Adaptor.sol"; +import {SnowbridgeL2Adaptor} from "../src/l2-integration/SnowbridgeL2Adaptor.sol"; +import {DepositParams, SendParams, SwapParams} from "../src/l2-integration/Types.sol"; +import {MockSpokePool, MockSpokePoolReverting} from "./mocks/MockSpokePool.sol"; +import {MockMessageHandler} from "./mocks/MockMessageHandler.sol"; +import {SnowbridgeL2TestLib} from "./GatewayV2SnowbridgeL2TestLib.sol"; +import {FakeAgent} from "./mocks/FakeAgent.sol"; + +contract GatewayV2SnowbridgeL2Test is Test { + address public assetHubAgent; + address public relayer; + bytes32 public relayerRewardAddress = keccak256("relayerRewardAddress"); + bytes32[] public proof = + [bytes32(0x2f9ee6cfdf244060dc28aa46347c5219e303fc95062dd672b4e406ca5c29764b)]; + + MockGateway public gatewayLogic; + GatewayProxy public gateway; + WETH9 public weth; + address public user1; + + function setUp() public { + weth = new WETH9(); + AgentExecutor executor = new AgentExecutor(); + gatewayLogic = new MockGateway(address(0), address(executor)); + Initializer.Config memory config = Initializer.Config({ + mode: OperatingMode.Normal, + deliveryCost: 1e10, + registerTokenFee: 0, + assetHubCreateAssetFee: 1e10, + assetHubReserveTransferFee: 1e10, + exchangeRate: ud60x18(0.0025e18), + multiplier: ud60x18(1e18), + foreignTokenDecimals: 10, + maxDestinationFee: 1e11 + }); + gateway = new GatewayProxy(address(gatewayLogic), abi.encode(config)); + MockGateway(address(gateway)).setCommitmentsAreVerified(true); + + SetOperatingModeParams memory params = SetOperatingModeParams({mode: OperatingMode.Normal}); + MockGateway(address(gateway)).v1_handleSetOperatingMode_public(abi.encode(params)); + + assetHubAgent = IGatewayV2(address(gateway)).agentOf(Constants.ASSET_HUB_AGENT_ID); + relayer = makeAddr("relayer"); + user1 = makeAddr("user1"); + + hoax(user1); + weth.deposit{value: 1 ether}(); + } + + function _makeMockProof() internal pure returns (Verification.Proof memory) { + return Verification.Proof({ + header: Verification.ParachainHeader({ + parentHash: bytes32(0), + number: 0, + stateRoot: bytes32(0), + extrinsicsRoot: bytes32(0), + digestItems: new Verification.DigestItem[](0) + }), + headProof: Verification.HeadProof({pos: 0, width: 0, proof: new bytes32[](0)}), + leafPartial: Verification.MMRLeafPartial({ + version: 0, + parentNumber: 0, + parentHash: bytes32(0), + nextAuthoritySetID: 0, + nextAuthoritySetLen: 0, + nextAuthoritySetRoot: 0 + }), + leafProof: new bytes32[](0), + leafProofOrder: 0 + }); + } + + function _deployL1AdaptorWithMockSpokePool() + internal + returns (MockSpokePool mockSpokePool, SnowbridgeL1Adaptor adaptor) + { + mockSpokePool = new MockSpokePool(); + adaptor = new SnowbridgeL1Adaptor(address(mockSpokePool), address(weth), address(gateway)); + } + + function _deployL1AdaptorWithRevertingSpokePool() + internal + returns (MockSpokePoolReverting mockSpokePool, SnowbridgeL1Adaptor adaptor) + { + mockSpokePool = new MockSpokePoolReverting(); + adaptor = new SnowbridgeL1Adaptor(address(mockSpokePool), address(weth), address(gateway)); + } + + function _deployL2AdaptorWithMockSpokePool() + internal + returns (MockSpokePool mockSpokePool, SnowbridgeL2Adaptor adaptor) + { + mockSpokePool = new MockSpokePool(); + MockMessageHandler handler = new MockMessageHandler(); + adaptor = new SnowbridgeL2Adaptor( + address(mockSpokePool), + address(handler), + address(gateway), + address(weth), + address(weth) + ); + } + + function testAgentCallsSnowbridgeL1AdaptorDepositTokenSuccess() public { + (MockSpokePool mockSpokePool, SnowbridgeL1Adaptor adaptor) = + _deployL1AdaptorWithMockSpokePool(); + uint256 inputAmount = 1 ether; + address recipient = makeAddr("recipient"); + + DepositParams memory params = + SnowbridgeL2TestLib.makeDepositParamsToken(address(weth), inputAmount, 0.9 ether); + bytes32 topic = keccak256("snowbridge-topic"); + + // Fund the agent so UnlockNativeToken can transfer to the adaptor + hoax(user1); + weth.transfer(assetHubAgent, inputAmount); + + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + SnowbridgeL2TestLib.makeDepositTokenMessageWithPrefund( + address(adaptor), params, recipient, topic, address(weth), uint128(inputAmount) + ), + proof, + _makeMockProof(), + relayerRewardAddress + ); + + assertEq(mockSpokePool.numberOfDeposits(), 1); + assertEq(weth.balanceOf(recipient), inputAmount); + } + + function testAgentCallsSnowbridgeL1AdaptorDepositTokenRevertsWhenCallerNotAgent() public { + (, SnowbridgeL1Adaptor adaptor) = _deployL1AdaptorWithMockSpokePool(); + uint256 inputAmount = 1 ether; + hoax(user1); + weth.transfer(address(adaptor), inputAmount); + + DepositParams memory params = + SnowbridgeL2TestLib.makeDepositParamsToken(address(weth), inputAmount, 0.9 ether); + bytes32 topic = keccak256("topic"); + address recipient = makeAddr("recipient"); + + vm.expectRevert(); + hoax(user1); + adaptor.depositToken(params, recipient, topic); + } + + function testAgentCallsSnowbridgeL1AdaptorDepositTokenSpokePoolRevertsEmitsDepositCallFailed() + public + { + (MockSpokePoolReverting mockSpokePool, SnowbridgeL1Adaptor adaptor) = + _deployL1AdaptorWithRevertingSpokePool(); + uint256 inputAmount = 1 ether; + address recipient = makeAddr("recipient"); + hoax(user1); + weth.transfer(address(adaptor), inputAmount); + + DepositParams memory params = + SnowbridgeL2TestLib.makeDepositParamsToken(address(weth), inputAmount, 0.9 ether); + bytes32 topic = keccak256("topic"); + + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + SnowbridgeL2TestLib.makeDepositTokenMessage( + address(adaptor), params, recipient, topic + ), + proof, + _makeMockProof(), + relayerRewardAddress + ); + + assertEq(mockSpokePool.numberOfDeposits(), 0); + assertEq(weth.balanceOf(recipient), inputAmount); + } + + function testAgentCallsSnowbridgeL1AdaptorDepositNativeEtherSuccess() public { + (MockSpokePool mockSpokePool, SnowbridgeL1Adaptor adaptor) = + _deployL1AdaptorWithMockSpokePool(); + uint256 inputAmount = 0.5 ether; + address recipient = makeAddr("recipient"); + + DepositParams memory params = + SnowbridgeL2TestLib.makeDepositParamsNativeEther(inputAmount, 0.4 ether); + bytes32 topic = keccak256("snowbridge-native"); + + vm.deal(assetHubAgent, inputAmount); + + hoax(relayer, 1 ether); + IGatewayV2(address(gateway)) + .v2_submit( + SnowbridgeL2TestLib.makeDepositNativeEtherMessageWithPrefund( + address(adaptor), params, recipient, topic, uint128(inputAmount) + ), + proof, + _makeMockProof(), + relayerRewardAddress + ); + + assertEq(mockSpokePool.numberOfDeposits(), 1); + assertEq(address(mockSpokePool).balance, inputAmount); + } + + function testL2AdaptorSendEtherAndCallSuccess() public { + (MockSpokePool mockSpokePool, SnowbridgeL2Adaptor adaptor) = + _deployL2AdaptorWithMockSpokePool(); + uint256 inputAmount = 1 ether; + uint128 executionFee = 0.05 ether; + uint128 relayerFee = 0.05 ether; + uint256 outputAmount = 0.88 ether; // so totalOutputAmount = 0.88 + 0.05 + 0.05 = 0.98 < inputAmount + address recipient = makeAddr("recipient"); + bytes32 topic = keccak256("l2-ether"); + + DepositParams memory params = DepositParams({ + inputToken: address(0), + outputToken: address(0x1234), + inputAmount: inputAmount, + outputAmount: outputAmount, + destinationChainId: 8453, + fillDeadlineBuffer: 600 + }); + SendParams memory sendParams = SendParams({ + xcm: "", + assets: new bytes[](0), + claimer: "", + executionFee: executionFee, + relayerFee: relayerFee + }); + + vm.expectEmit(true, true, false, true); + emit SnowbridgeL2Adaptor.DepositCallInvoked(topic, 0); + + hoax(user1, inputAmount); + adaptor.sendEtherAndCall{value: inputAmount}(params, sendParams, recipient, topic); + + assertEq(mockSpokePool.numberOfDeposits(), 1); + } + + function testL2AdaptorSendTokenAndCallSuccess() public { + (MockSpokePool mockSpokePool, SnowbridgeL2Adaptor adaptor) = + _deployL2AdaptorWithMockSpokePool(); + uint256 inputAmount = 1 ether; + uint256 outputAmount = 0.8 ether; + uint256 swapInputAmount = 0.1 ether; + address recipient = makeAddr("recipient"); + bytes32 topic = keccak256("l2-token"); + + DepositParams memory params = DepositParams({ + inputToken: address(weth), + outputToken: address(weth), + inputAmount: inputAmount, + outputAmount: outputAmount, + destinationChainId: 8453, + fillDeadlineBuffer: 600 + }); + SwapParams memory swapParams = SwapParams({ + inputAmount: swapInputAmount, + router: address(0x1234), + callData: hex"01" + }); + SendParams memory sendParams = SendParams({ + xcm: "", + assets: new bytes[](0), + claimer: "", + executionFee: 0.05 ether, + relayerFee: 0.05 ether + }); + + hoax(user1); + weth.approve(address(adaptor), inputAmount); + + vm.expectEmit(true, true, false, true); + emit SnowbridgeL2Adaptor.DepositCallInvoked(topic, 0); + + hoax(user1); + adaptor.sendTokenAndCall(params, swapParams, sendParams, recipient, topic); + + assertEq(mockSpokePool.numberOfDeposits(), 1); + } + + function testL2AdaptorSendEtherAndCallRevertsWhenInputTokenNotZeroOrWeth() public { + (, SnowbridgeL2Adaptor adaptor) = _deployL2AdaptorWithMockSpokePool(); + address recipient = makeAddr("recipient"); + address invalidToken = address(0x1234); // neither address(0) nor L2_WETH9 + + DepositParams memory params = DepositParams({ + inputToken: invalidToken, + outputToken: address(0x1234), + inputAmount: 1 ether, + outputAmount: 0.88 ether, + destinationChainId: 8453, + fillDeadlineBuffer: 600 + }); + SendParams memory sendParams = SendParams({ + xcm: "", + assets: new bytes[](0), + claimer: "", + executionFee: 0.05 ether, + relayerFee: 0.05 ether + }); + + vm.expectRevert( + "Input token must be zero address or L2 WETH address for native ETH deposits" + ); + adaptor.sendEtherAndCall(params, sendParams, recipient, keccak256("topic")); + } + + function testL2AdaptorSendEtherAndCallRevertsWhenWethDepositWithValue() public { + (, SnowbridgeL2Adaptor adaptor) = _deployL2AdaptorWithMockSpokePool(); + uint256 inputAmount = 1 ether; + address recipient = makeAddr("recipient"); + + DepositParams memory params = DepositParams({ + inputToken: address(weth), // WETH path + outputToken: address(0x1234), + inputAmount: inputAmount, + outputAmount: 0.88 ether, + destinationChainId: 8453, + fillDeadlineBuffer: 600 + }); + SendParams memory sendParams = SendParams({ + xcm: "", + assets: new bytes[](0), + claimer: "", + executionFee: 0.05 ether, + relayerFee: 0.05 ether + }); + + hoax(user1); + weth.approve(address(adaptor), inputAmount); + + vm.expectRevert("Sent value must be zero for WETH deposits"); + adaptor.sendEtherAndCall{value: 1 ether}(params, sendParams, recipient, keccak256("topic")); + } + + function testFakeAgentCanBypassOnlyRegisteredGatewayAgentModifier() public { + (MockSpokePool mockSpokePool, SnowbridgeL1Adaptor adaptor) = + _deployL1AdaptorWithMockSpokePool(); + uint256 inputAmount = 1 ether; + address recipient = makeAddr("recipient"); + + // Deploy a fake agent that returns the real gateway address from GATEWAY() + FakeAgent fakeAgent = new FakeAgent(address(gateway)); + + // Fund the fake agent with tokens (simulating a scenario where the adaptor holds tokens) + hoax(user1); + weth.transfer(address(adaptor), inputAmount); + + DepositParams memory params = + SnowbridgeL2TestLib.makeDepositParamsToken(address(weth), inputAmount, 0.9 ether); + bytes32 topic = keccak256("fake-agent-topic"); + + // The fake agent bypasses the onlyRegisteredGatewayAgent modifier + // because it exposes GATEWAY() returning the real gateway address. + // This test documents the spoofability — it should FAIL once the modifier + // is strengthened to verify against the Gateway's agent registry. + fakeAgent.callDepositToken(adaptor, params, recipient, topic); + + // If we reach here, the fake agent successfully called depositToken + assertEq(mockSpokePool.numberOfDeposits(), 1); + } +} diff --git a/contracts/test/GatewayV2SnowbridgeL2TestLib.sol b/contracts/test/GatewayV2SnowbridgeL2TestLib.sol new file mode 100644 index 000000000..36a268059 --- /dev/null +++ b/contracts/test/GatewayV2SnowbridgeL2TestLib.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.33; + +import {SnowbridgeL1Adaptor} from "../src/l2-integration/SnowbridgeL1Adaptor.sol"; +import {DepositParams} from "../src/l2-integration/Types.sol"; +import {CallContractParams, UnlockNativeTokenParams} from "../src/v2/Types.sol"; +import {CommandV2, CommandKind, InboundMessageV2} from "../src/Types.sol"; +import {Constants} from "../src/Constants.sol"; + +library SnowbridgeL2TestLib { + function makeUnlockTokenCommand(address token, address recipient, uint128 amount) + internal + pure + returns (CommandV2 memory) + { + UnlockNativeTokenParams memory params = + UnlockNativeTokenParams({token: token, recipient: recipient, amount: amount}); + return CommandV2({ + kind: CommandKind.UnlockNativeToken, + gas: 500_000, + payload: abi.encode(params) + }); + } + + function makeDepositTokenCommand( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic + ) internal pure returns (CommandV2[] memory) { + bytes memory data = abi.encodeWithSelector( + SnowbridgeL1Adaptor.depositToken.selector, params, recipient, topic + ); + CallContractParams memory callParams = + CallContractParams({target: adaptor, data: data, value: 0}); + CommandV2[] memory commands = new CommandV2[](1); + commands[0] = CommandV2({ + kind: CommandKind.CallContract, gas: 500_000, payload: abi.encode(callParams) + }); + return commands; + } + + /// Builds commands: first UnlockNativeToken to prefund the adaptor, then CallContract to depositToken. + function makeDepositTokenCommandWithPrefund( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic, + address prefundToken, + uint128 prefundAmount + ) internal pure returns (CommandV2[] memory) { + CommandV2[] memory commands = new CommandV2[](2); + commands[0] = makeUnlockTokenCommand(prefundToken, adaptor, prefundAmount); + bytes memory data = abi.encodeWithSelector( + SnowbridgeL1Adaptor.depositToken.selector, params, recipient, topic + ); + CallContractParams memory callParams = + CallContractParams({target: adaptor, data: data, value: 0}); + commands[1] = CommandV2({ + kind: CommandKind.CallContract, gas: 500_000, payload: abi.encode(callParams) + }); + return commands; + } + + function makeDepositParamsToken(address inputToken, uint256 inputAmount, uint256 outputAmount) + internal + pure + returns (DepositParams memory) + { + return DepositParams({ + inputToken: inputToken, + outputToken: address(0x1234), + inputAmount: inputAmount, + outputAmount: outputAmount, + destinationChainId: 8453, + fillDeadlineBuffer: 600 + }); + } + + function makeDepositParamsNativeEther(uint256 inputAmount, uint256 outputAmount) + internal + pure + returns (DepositParams memory) + { + return DepositParams({ + inputToken: address(0), + outputToken: address(0x1234), + inputAmount: inputAmount, + outputAmount: outputAmount, + destinationChainId: 8453, + fillDeadlineBuffer: 600 + }); + } + + function makeDepositNativeEtherCommand( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic + ) internal pure returns (CommandV2 memory) { + bytes memory data = abi.encodeWithSelector( + SnowbridgeL1Adaptor.depositNativeEther.selector, params, recipient, topic + ); + CallContractParams memory callParams = + CallContractParams({target: adaptor, data: data, value: 0}); + return CommandV2({ + kind: CommandKind.CallContract, gas: 500_000, payload: abi.encode(callParams) + }); + } + + /// Builds commands: UnlockNativeToken (native ETH) to prefund the adaptor, then CallContract depositNativeEther. + function makeDepositNativeEtherCommandWithPrefund( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic, + uint128 prefundAmount + ) internal pure returns (CommandV2[] memory) { + CommandV2[] memory commands = new CommandV2[](2); + commands[0] = makeUnlockTokenCommand(address(0), adaptor, prefundAmount); + commands[1] = makeDepositNativeEtherCommand(adaptor, params, recipient, topic); + return commands; + } + + function makeDepositNativeEtherMessageWithPrefund( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic, + uint128 prefundAmount + ) internal pure returns (InboundMessageV2 memory) { + return InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeDepositNativeEtherCommandWithPrefund( + adaptor, params, recipient, topic, prefundAmount + ) + }); + } + + function makeDepositTokenMessage( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic + ) internal pure returns (InboundMessageV2 memory) { + return InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeDepositTokenCommand(adaptor, params, recipient, topic) + }); + } + + function makeDepositTokenMessageWithPrefund( + address adaptor, + DepositParams memory params, + address recipient, + bytes32 topic, + address prefundToken, + uint128 prefundAmount + ) internal pure returns (InboundMessageV2 memory) { + return InboundMessageV2({ + origin: Constants.ASSET_HUB_AGENT_ID, + nonce: 1, + topic: topic, + commands: makeDepositTokenCommandWithPrefund( + adaptor, params, recipient, topic, prefundToken, prefundAmount + ) + }); + } +} diff --git a/contracts/test/mocks/FakeAgent.sol b/contracts/test/mocks/FakeAgent.sol new file mode 100644 index 000000000..896514f26 --- /dev/null +++ b/contracts/test/mocks/FakeAgent.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.33; + +import {SnowbridgeL1Adaptor} from "../../src/l2-integration/SnowbridgeL1Adaptor.sol"; +import {DepositParams} from "../../src/l2-integration/Types.sol"; +import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; + +/// @dev A contract that impersonates a Gateway Agent by exposing a GATEWAY() getter +/// returning the real gateway address, without actually being registered in the Gateway. +contract FakeAgent { + address public immutable GATEWAY; + + constructor(address _gateway) { + GATEWAY = _gateway; + } + + function callDepositToken( + SnowbridgeL1Adaptor adaptor, + DepositParams calldata params, + address recipient, + bytes32 topic + ) external { + adaptor.depositToken(params, recipient, topic); + } + + function callDepositNativeEther( + SnowbridgeL1Adaptor adaptor, + DepositParams calldata params, + address recipient, + bytes32 topic + ) external { + adaptor.depositNativeEther(params, recipient, topic); + } + + receive() external payable {} +} diff --git a/contracts/test/mocks/MockMessageHandler.sol b/contracts/test/mocks/MockMessageHandler.sol new file mode 100644 index 000000000..88706e07d --- /dev/null +++ b/contracts/test/mocks/MockMessageHandler.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.33; + +import {IMessageHandler} from "../../src/l2-integration/interfaces/ISpokePool.sol"; + +contract MockMessageHandler is IMessageHandler { + function handleV3AcrossMessage(address, uint256, address, bytes memory) external override {} +} diff --git a/contracts/test/mocks/MockSpokePool.sol b/contracts/test/mocks/MockSpokePool.sol new file mode 100644 index 000000000..c3cf5be8d --- /dev/null +++ b/contracts/test/mocks/MockSpokePool.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.33; + +import {ISpokePool} from "../../src/l2-integration/interfaces/ISpokePool.sol"; + +contract MockSpokePool is ISpokePool { + uint256 private _numberOfDeposits; + + function deposit( + bytes32, + bytes32, + bytes32, + bytes32, + uint256, + uint256, + uint256, + bytes32, + uint32, + uint32, + uint32, + bytes calldata + ) external payable override { + _numberOfDeposits++; + } + + function numberOfDeposits() external view override returns (uint256) { + return _numberOfDeposits; + } +} + +contract MockSpokePoolReverting is ISpokePool { + function deposit( + bytes32, + bytes32, + bytes32, + bytes32, + uint256, + uint256, + uint256, + bytes32, + uint32, + uint32, + uint32, + bytes calldata + ) external payable override { + revert("MockSpokePool: deposit reverted"); + } + + function numberOfDeposits() external view override returns (uint256) { + return 0; + } +}