diff --git a/contracts/test/GenericCallForwarder.integration.t.sol b/contracts/test/GenericCallForwarder.integration.t.sol deleted file mode 100644 index 6b62888..0000000 --- a/contracts/test/GenericCallForwarder.integration.t.sol +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.30; - -import {IERC20} from "@openzeppelin-contracts-5.6.1/token/ERC20/IERC20.sol"; -import {Time} from "@openzeppelin-contracts-5.6.1/utils/types/Time.sol"; -import {IForwarder} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/IForwarder.sol"; -import {INativeTokenReceiver} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/INativeTokenReceiver.sol"; -import {ERC20Forwarder} from "anomapay-erc20-forwarder-1.1.0-rc.2/src/ERC20Forwarder.sol"; -import {ERC20Example} from "anomapay-erc20-forwarder-1.1.0-rc.2/test/examples/ERC20.e.sol"; -import {DeployPermit2} from "anomapay-erc20-forwarder-1.1.0-rc.2/test/script/DeployPermit2.s.sol"; -import {Test} from "forge-std-1.16.1/src/Test.sol"; - -import {WETH} from "solady-0.1.26/src/tokens/WETH.sol"; -import { - IAllowanceTransfer -} from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IAllowanceTransfer.sol"; -import {IPermit2} from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IPermit2.sol"; - -import {GenericCallForwarder} from "../src/GenericCallForwarder.sol"; -import {DexRouterMock} from "./mocks/DexRouter.m.sol"; - -contract GenericCallForwarderTest is Test { - address internal constant _PROTOCOL_ADAPTER = address(uint160(1)); - address internal constant _EMERGENCY_COMMITTEE = address(uint160(2)); - uint128 internal constant _TRANSFER_AMOUNT = 1000; - bytes internal constant _EXPECTED_OUTPUT = ""; - bytes32 internal constant _ACTION_TREE_ROOT = bytes32(uint256(0)); - - bytes32 internal _erc20ResourceLogicRef; - bytes32 internal _genericCallResourceLogicRef; - - address internal _alice; - - IForwarder internal _erc20Fwd; - IForwarder internal _genericCallFwd; - - IPermit2 internal _permit2; - WETH internal _weth; - - bytes internal _defaultUnwrapInput; - - function setUp() public { - _erc20ResourceLogicRef = bytes32(uint256(1)); - _genericCallResourceLogicRef = bytes32(uint256(2)); - - _alice = makeAddr("alice"); - - _weth = new WETH(); - - // Deploy the Permit2 contract - _permit2 = new DeployPermit2().run(); - - // Deploy the generic call forwarder - _erc20Fwd = new ERC20Forwarder({ - protocolAdapter: _PROTOCOL_ADAPTER, - emergencyCommittee: _EMERGENCY_COMMITTEE, - logicRef: _erc20ResourceLogicRef - }); - - _genericCallFwd = - new GenericCallForwarder({protocolAdapter: _PROTOCOL_ADAPTER, logicRef: _genericCallResourceLogicRef}); - - _defaultUnwrapInput = abi.encode( /* callType */ - ERC20Forwarder.CallType.Unwrap, - /* token */ - address(_weth), - /* amount */ - _TRANSFER_AMOUNT, - /* unwrap data */ - ERC20Forwarder.UnwrapData({receiver: address(_genericCallFwd)}) - ); - } - - function test_calls_allow_to_swap_erc20s_on_a_dex() public { - uint128 minAmountOut = _TRANSFER_AMOUNT / 2; - ERC20Example tokenA = new ERC20Example(); - ERC20Example tokenB = new ERC20Example(); - DexRouterMock dexRouter = new DexRouterMock(address(_permit2)); - tokenB.mint(address(dexRouter), minAmountOut); - - // Fund ERC20Forwarder with tokenA - tokenA.mint(address(_erc20Fwd), _TRANSFER_AMOUNT); - - assertEq(tokenA.balanceOf(address(_erc20Fwd)), _TRANSFER_AMOUNT); - assertEq(tokenA.balanceOf(address(_genericCallFwd)), 0); - assertEq(tokenB.balanceOf(address(_genericCallFwd)), 0); - assertEq(tokenB.balanceOf(address(_erc20Fwd)), 0); - - // Unwrap tokenA from ERC20Forwarder into GenericCallForwarder - { - bytes memory unwrapInput = abi.encode( - ERC20Forwarder.CallType.Unwrap, - address(tokenA), - _TRANSFER_AMOUNT, - ERC20Forwarder.UnwrapData({receiver: address(_genericCallFwd)}) - ); - - vm.prank(_PROTOCOL_ADAPTER); - bytes memory output1 = _erc20Fwd.forwardCall({logicRef: _erc20ResourceLogicRef, input: unwrapInput}); - assertEq(keccak256(output1), keccak256(_EXPECTED_OUTPUT)); - } - - assertEq(tokenA.balanceOf(address(_erc20Fwd)), 0); - assertEq(tokenA.balanceOf(address(_genericCallFwd)), _TRANSFER_AMOUNT); - - // Swap tokenA for tokenB via MockDexRouter and approve Permit2 to pull tokenB for the subsequent wrap - { - uint48 expiration = uint48(Time.timestamp() + 5 minutes); - - address[] memory path = new address[](2); - path[0] = address(tokenA); - path[1] = address(tokenB); - - GenericCallForwarder.Call[] memory calls = new GenericCallForwarder.Call[](4); - - // 3a: Approve Permit2 to spend tokenA, then grant DEX router a Permit2 allowance to pull it - calls[0] = GenericCallForwarder.Call({ - to: address(tokenA), - value: 0, - data: abi.encodeCall(IERC20.approve, (address(_permit2), _TRANSFER_AMOUNT)) - }); - calls[1] = GenericCallForwarder.Call({ - to: address(_permit2), - value: 0, - data: abi.encodeCall( - IAllowanceTransfer.approve, - (address(tokenA), address(dexRouter), uint160(_TRANSFER_AMOUNT), expiration) - ) - }); - - // 3b: Swap _TRANSFER_AMOUNT of tokenA for minAmountOut of tokenB - calls[2] = GenericCallForwarder.Call({ - to: address(dexRouter), - value: 0, - data: abi.encodeCall( - DexRouterMock.swapExactTokensForTokens, - (_TRANSFER_AMOUNT, minAmountOut, path, address(_genericCallFwd), expiration) - ) - }); - - // 3c: Approve Permit2 to spend tokenB so ERC20Forwarder can wrap it via permitWitnessTransferFrom - calls[3] = GenericCallForwarder.Call({ - to: address(tokenB), value: 0, data: abi.encodeCall(IERC20.approve, (address(_permit2), minAmountOut)) - }); - - vm.prank(_PROTOCOL_ADAPTER); - bytes memory output2 = - _genericCallFwd.forwardCall({logicRef: _genericCallResourceLogicRef, input: abi.encode(calls)}); - assertEq(keccak256(output2), keccak256(_EXPECTED_OUTPUT)); - } - - assertEq(tokenA.balanceOf(address(_genericCallFwd)), 0); - assertEq(tokenB.balanceOf(address(_genericCallFwd)), minAmountOut); - assertEq(tokenB.balanceOf(address(_erc20Fwd)), 0); - - // Wrap tokenB from GenericCallForwarder into ERC20Forwarder. - // GenericCallForwarder implements ERC-1271 and always returns the magic value, so any r, s, v bytes are valid. - { - bytes memory wrapTokenBInput = abi.encode( - ERC20Forwarder.CallType.Wrap, - address(tokenB), - minAmountOut, - ERC20Forwarder.WrapData({ - nonce: 456, - deadline: Time.timestamp() + 5 minutes, - owner: address(_genericCallFwd), - actionTreeRoot: _ACTION_TREE_ROOT, - r: bytes32(0), - s: bytes32(0), - v: 27 - }) - ); - - vm.prank(_PROTOCOL_ADAPTER); - bytes memory output3 = _erc20Fwd.forwardCall({logicRef: _erc20ResourceLogicRef, input: wrapTokenBInput}); - assertEq(keccak256(output3), keccak256(_EXPECTED_OUTPUT)); - } - - assertEq(tokenB.balanceOf(address(_genericCallFwd)), 0); - assertEq(tokenB.balanceOf(address(_erc20Fwd)), minAmountOut); - } - - function test_calls_allow_to_unwrap_native_tokens() public { - // Fund Generic Call Forwarder with WETH - { - vm.deal(address(_erc20Fwd), _TRANSFER_AMOUNT); - vm.prank(address(_erc20Fwd)); - _weth.deposit{value: _TRANSFER_AMOUNT}(); - } - - assertEq(_weth.balanceOf(address(_erc20Fwd)), _TRANSFER_AMOUNT); - assertEq(_weth.balanceOf(address(_genericCallFwd)), 0); - assertEq(_alice.balance, 0); - - // Mock ERC20Forwarder call (triggered by TokenTransfer resource) - { - // Unwrap WETH-R into the generic call forwarder - vm.prank(_PROTOCOL_ADAPTER); - bytes memory output1 = _erc20Fwd.forwardCall({logicRef: _erc20ResourceLogicRef, input: _defaultUnwrapInput}); - assertEq(keccak256(output1), keccak256(_EXPECTED_OUTPUT)); - } - - assertEq(_weth.balanceOf(address(_erc20Fwd)), 0); - assertEq(_weth.balanceOf(address(_genericCallFwd)), _TRANSFER_AMOUNT); - assertEq(_alice.balance, 0); - - // Mock GenericCallForwarder call (triggered by GenericCall resource) - { - GenericCallForwarder.Call[] memory genericCalls = new GenericCallForwarder.Call[](2); - // Call 1: Unwrap WETH - genericCalls[0] = GenericCallForwarder.Call({ - to: address(_weth), value: 0, data: abi.encodeCall(WETH.withdraw, uint256(_TRANSFER_AMOUNT)) - }); - // Call 2: Transfer ETH - genericCalls[1] = GenericCallForwarder.Call({to: _alice, value: _TRANSFER_AMOUNT, data: ""}); - bytes memory _unwrapWethAndTransferEthInput = abi.encode(genericCalls); - - vm.prank(_PROTOCOL_ADAPTER); - vm.expectEmit(address(_genericCallFwd)); - emit INativeTokenReceiver.NativeTokenReceived({sender: address(_weth), amount: _TRANSFER_AMOUNT}); - - bytes memory output2 = _genericCallFwd.forwardCall({ - logicRef: _genericCallResourceLogicRef, input: _unwrapWethAndTransferEthInput - }); - assertEq(keccak256(output2), keccak256(_EXPECTED_OUTPUT)); - } - - assertEq(_weth.balanceOf(address(_erc20Fwd)), 0); - assertEq(_weth.balanceOf(address(_genericCallFwd)), 0); - assertEq(_alice.balance, _TRANSFER_AMOUNT); - } -} - diff --git a/contracts/test/GenericCallForwarder.unit.t.sol b/contracts/test/GenericCallForwarder.unit.t.sol index 8913365..02991f7 100644 --- a/contracts/test/GenericCallForwarder.unit.t.sol +++ b/contracts/test/GenericCallForwarder.unit.t.sol @@ -6,7 +6,6 @@ import {IERC1271} from "@openzeppelin-contracts-5.6.1/interfaces/IERC1271.sol"; import {IERC20} from "@openzeppelin-contracts-5.6.1/token/ERC20/IERC20.sol"; import {Errors} from "@openzeppelin-contracts-5.6.1/utils/Errors.sol"; import {ReentrancyGuardTransient} from "@openzeppelin-contracts-5.6.1/utils/ReentrancyGuardTransient.sol"; -import {IForwarder} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/IForwarder.sol"; import {IVersion} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/IVersion.sol"; import {ERC20Example} from "anomapay-erc20-forwarder-1.1.0-rc.2/test/examples/ERC20.e.sol"; import {Test} from "forge-std-1.16.1/src/Test.sol"; @@ -26,7 +25,7 @@ contract GenericCallForwarderTest is Test { address internal _alice; - IForwarder internal _genericCallFwd; + GenericCallForwarder internal _genericCallFwd; WETH internal _weth; @@ -182,12 +181,11 @@ contract GenericCallForwarderTest is Test { _genericCallFwd.forwardCall({logicRef: _genericCallResourceLogicRef, input: abi.encode(calls)}); } - function test_isValidSignature_always_returns_the_ERC1271_magic_value() public view { - bytes4 magic = IERC1271.isValidSignature.selector; - - assertEq(IERC1271(address(_genericCallFwd)).isValidSignature(bytes32(0), ""), magic); - assertEq(IERC1271(address(_genericCallFwd)).isValidSignature(keccak256("anoma"), hex"cafe"), magic); - assertEq(IERC1271(address(_genericCallFwd)).isValidSignature(bytes32(type(uint256).max), hex"00"), magic); + function testFuzz_isValidSignature_always_returns_the_ERC1271_magic_value(bytes32 hash, bytes calldata signature) + public + view + { + assertEq(_genericCallFwd.isValidSignature(hash, signature), IERC1271.isValidSignature.selector); } function test_check_that_the_current_version_is_a_pre_release_of_v1_0_0() public view { diff --git a/contracts/test/integration/GenericCallForwarder.native.t.sol b/contracts/test/integration/GenericCallForwarder.native.t.sol new file mode 100644 index 0000000..385dba7 --- /dev/null +++ b/contracts/test/integration/GenericCallForwarder.native.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {IForwarder} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/IForwarder.sol"; +import {INativeTokenReceiver} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/INativeTokenReceiver.sol"; +import {ERC20Forwarder} from "anomapay-erc20-forwarder-1.1.0-rc.2/src/ERC20Forwarder.sol"; +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {WETH} from "solady-0.1.26/src/tokens/WETH.sol"; + +import {GenericCallForwarder} from "../../src/GenericCallForwarder.sol"; + +contract GenericCallForwarderNativeTest is Test { + address internal constant _PROTOCOL_ADAPTER = address(uint160(1)); + address internal constant _EMERGENCY_COMMITTEE = address(uint160(2)); + uint128 internal constant _TRANSFER_AMOUNT = 1000; + bytes internal constant _EXPECTED_OUTPUT = ""; + + bytes32 internal _erc20ResourceLogicRef; + bytes32 internal _genericCallResourceLogicRef; + + address internal _alice; + + IForwarder internal _erc20Fwd; + IForwarder internal _genericCallFwd; + + WETH internal _weth; + + bytes internal _defaultUnwrapInput; + + function setUp() public { + _erc20ResourceLogicRef = bytes32(uint256(1)); + _genericCallResourceLogicRef = bytes32(uint256(2)); + + _alice = makeAddr("alice"); + + _weth = new WETH(); + + // Deploy the forwarders + _erc20Fwd = new ERC20Forwarder({ + protocolAdapter: _PROTOCOL_ADAPTER, + emergencyCommittee: _EMERGENCY_COMMITTEE, + logicRef: _erc20ResourceLogicRef + }); + _genericCallFwd = + new GenericCallForwarder({protocolAdapter: _PROTOCOL_ADAPTER, logicRef: _genericCallResourceLogicRef}); + + _defaultUnwrapInput = abi.encode( /* callType */ + ERC20Forwarder.CallType.Unwrap, + /* token */ + address(_weth), + /* amount */ + _TRANSFER_AMOUNT, + /* unwrap data */ + ERC20Forwarder.UnwrapData({receiver: address(_genericCallFwd)}) + ); + } + + function test_calls_allow_to_unwrap_native_tokens() public { + // Fund Generic Call Forwarder with WETH + { + vm.deal(address(_erc20Fwd), _TRANSFER_AMOUNT); + vm.prank(address(_erc20Fwd)); + _weth.deposit{value: _TRANSFER_AMOUNT}(); + } + + assertEq(_weth.balanceOf(address(_erc20Fwd)), _TRANSFER_AMOUNT); + assertEq(_weth.balanceOf(address(_genericCallFwd)), 0); + assertEq(_alice.balance, 0); + + // Mock ERC20Forwarder call (triggered by TokenTransfer resource) + { + // Unwrap WETH-R into the generic call forwarder + vm.prank(_PROTOCOL_ADAPTER); + bytes memory output1 = _erc20Fwd.forwardCall({logicRef: _erc20ResourceLogicRef, input: _defaultUnwrapInput}); + assertEq(keccak256(output1), keccak256(_EXPECTED_OUTPUT)); + } + + assertEq(_weth.balanceOf(address(_erc20Fwd)), 0); + assertEq(_weth.balanceOf(address(_genericCallFwd)), _TRANSFER_AMOUNT); + assertEq(_alice.balance, 0); + + // Mock GenericCallForwarder call (triggered by GenericCall resource) + { + GenericCallForwarder.Call[] memory genericCalls = new GenericCallForwarder.Call[](2); + // Call 1: Unwrap WETH + genericCalls[0] = GenericCallForwarder.Call({ + to: address(_weth), value: 0, data: abi.encodeCall(WETH.withdraw, uint256(_TRANSFER_AMOUNT)) + }); + // Call 2: Transfer ETH + genericCalls[1] = GenericCallForwarder.Call({to: _alice, value: _TRANSFER_AMOUNT, data: ""}); + bytes memory _unwrapWethAndTransferEthInput = abi.encode(genericCalls); + + vm.prank(_PROTOCOL_ADAPTER); + vm.expectEmit(address(_genericCallFwd)); + emit INativeTokenReceiver.NativeTokenReceived({sender: address(_weth), amount: _TRANSFER_AMOUNT}); + + bytes memory output2 = _genericCallFwd.forwardCall({ + logicRef: _genericCallResourceLogicRef, input: _unwrapWethAndTransferEthInput + }); + assertEq(keccak256(output2), keccak256(_EXPECTED_OUTPUT)); + } + + assertEq(_weth.balanceOf(address(_erc20Fwd)), 0); + assertEq(_weth.balanceOf(address(_genericCallFwd)), 0); + assertEq(_alice.balance, _TRANSFER_AMOUNT); + } +} diff --git a/contracts/test/integration/GenericCallForwarder.swap.t.sol b/contracts/test/integration/GenericCallForwarder.swap.t.sol new file mode 100644 index 0000000..7547436 --- /dev/null +++ b/contracts/test/integration/GenericCallForwarder.swap.t.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {IERC20} from "@openzeppelin-contracts-5.6.1/token/ERC20/IERC20.sol"; +import {Time} from "@openzeppelin-contracts-5.6.1/utils/types/Time.sol"; +import {IForwarder} from "anoma-forwarder-bases-1.0.0-rc.3/src/interfaces/IForwarder.sol"; +import {ERC20Forwarder} from "anomapay-erc20-forwarder-1.1.0-rc.2/src/ERC20Forwarder.sol"; +import {ERC20Example} from "anomapay-erc20-forwarder-1.1.0-rc.2/test/examples/ERC20.e.sol"; +import {DeployPermit2} from "anomapay-erc20-forwarder-1.1.0-rc.2/test/script/DeployPermit2.s.sol"; +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import { + IAllowanceTransfer +} from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IAllowanceTransfer.sol"; +import {IPermit2} from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IPermit2.sol"; +import { + ISignatureTransfer +} from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/ISignatureTransfer.sol"; + +import {GenericCallForwarder} from "../../src/GenericCallForwarder.sol"; +import {DexRouterMock} from "../mocks/DexRouter.m.sol"; + +contract GenericCallForwarderSwapTest is Test { + address internal constant _PROTOCOL_ADAPTER = address(uint160(1)); + address internal constant _EMERGENCY_COMMITTEE = address(uint160(2)); + uint128 internal constant _TRANSFER_AMOUNT = 1000; + bytes internal constant _EXPECTED_OUTPUT = ""; + bytes32 internal constant _ACTION_TREE_ROOT = bytes32(uint256(0)); + + bytes32 internal _erc20ResourceLogicRef; + bytes32 internal _genericCallResourceLogicRef; + + IForwarder internal _erc20Fwd; + IForwarder internal _genericCallFwd; + + IPermit2 internal _permit2; + + ERC20Example internal _tokenA; + ERC20Example internal _tokenB; + DexRouterMock internal _dexRouter; + uint128 internal _minAmountOut; + + function setUp() public { + _erc20ResourceLogicRef = bytes32(uint256(1)); + _genericCallResourceLogicRef = bytes32(uint256(2)); + + // Deploy the Permit2 contract + _permit2 = new DeployPermit2().run(); + + // Deploy the forwarders + _erc20Fwd = new ERC20Forwarder({ + protocolAdapter: _PROTOCOL_ADAPTER, + emergencyCommittee: _EMERGENCY_COMMITTEE, + logicRef: _erc20ResourceLogicRef + }); + _genericCallFwd = + new GenericCallForwarder({protocolAdapter: _PROTOCOL_ADAPTER, logicRef: _genericCallResourceLogicRef}); + + // Deploy the swap tokens and the DEX router, fund the router with the output token and the ERC20 forwarder + // with the input token, and assert the initial balances. + _minAmountOut = _TRANSFER_AMOUNT * 4 / 5; + _tokenA = new ERC20Example(); + _tokenB = new ERC20Example(); + _dexRouter = new DexRouterMock(address(_permit2)); + _tokenB.mint(address(_dexRouter), _minAmountOut); + + _tokenA.mint(address(_erc20Fwd), _TRANSFER_AMOUNT); + + assertEq(_tokenA.balanceOf(address(_erc20Fwd)), _TRANSFER_AMOUNT); + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), 0); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), 0); + assertEq(_tokenB.balanceOf(address(_erc20Fwd)), 0); + } + + /// @notice Swaps where the DEX router pulls the input token via a classic ERC-20 allowance + /// (`approve` + `transferFrom`). + function test_calls_allow_to_swap_erc20s_on_a_dex_with_erc20_approval() public { + _unwrapTokenIntoGenericCallForwarder({token: _tokenA, amount: _TRANSFER_AMOUNT}); + + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), _TRANSFER_AMOUNT); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), 0); + { + // Approve the DEX router to spend tokenA, swap it for tokenB, and approve Permit2 to pull tokenB for the + // wrap. + GenericCallForwarder.Call[] memory calls = new GenericCallForwarder.Call[](3); + calls[0] = GenericCallForwarder.Call({ + to: address(_tokenA), + value: 0, + data: abi.encodeCall(IERC20.approve, (address(_dexRouter), _TRANSFER_AMOUNT)) + }); + calls[1] = GenericCallForwarder.Call({ + to: address(_dexRouter), + value: 0, + data: abi.encodeCall( + DexRouterMock.swapExactTokensForTokensWithErc20Approval, + (_TRANSFER_AMOUNT, _minAmountOut, address(_tokenA), address(_tokenB), address(_genericCallFwd)) + ) + }); + calls[2] = GenericCallForwarder.Call({ + to: address(_tokenB), value: 0, data: abi.encodeCall(IERC20.approve, (address(_permit2), _minAmountOut)) + }); + + vm.prank(_PROTOCOL_ADAPTER); + _genericCallFwd.forwardCall({logicRef: _genericCallResourceLogicRef, input: abi.encode(calls)}); + } + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), 0); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), _minAmountOut); + + _wrapTokenFromGenericCallForwarder({token: _tokenB, amount: _minAmountOut}); + } + + /// @notice Swaps where the DEX router pulls the input token via a Permit2 allowance + /// (`IAllowanceTransfer.approve` + `transferFrom`). + function test_calls_allow_to_swap_erc20s_on_a_dex_with_permit2_allowance() public { + _unwrapTokenIntoGenericCallForwarder({token: _tokenA, amount: _TRANSFER_AMOUNT}); + + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), _TRANSFER_AMOUNT); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), 0); + { + // Approve Permit2 to spend tokenA, grant the DEX router a Permit2 allowance to pull it, swap it for tokenB, + // and approve Permit2 to pull tokenB for the subsequent wrap. + GenericCallForwarder.Call[] memory calls = new GenericCallForwarder.Call[](4); + calls[0] = GenericCallForwarder.Call({ + to: address(_tokenA), + value: 0, + data: abi.encodeCall(IERC20.approve, (address(_permit2), _TRANSFER_AMOUNT)) + }); + calls[1] = GenericCallForwarder.Call({ + to: address(_permit2), + value: 0, + data: abi.encodeCall( + IAllowanceTransfer.approve, + (address(_tokenA), address(_dexRouter), uint160(_TRANSFER_AMOUNT), uint48(_swapDeadline())) + ) + }); + calls[2] = GenericCallForwarder.Call({ + to: address(_dexRouter), + value: 0, + data: abi.encodeCall( + DexRouterMock.swapExactTokensForTokensWithPermit2Allowance, + (_TRANSFER_AMOUNT, _minAmountOut, address(_tokenA), address(_tokenB), address(_genericCallFwd)) + ) + }); + calls[3] = GenericCallForwarder.Call({ + to: address(_tokenB), value: 0, data: abi.encodeCall(IERC20.approve, (address(_permit2), _minAmountOut)) + }); + + vm.prank(_PROTOCOL_ADAPTER); + _genericCallFwd.forwardCall({logicRef: _genericCallResourceLogicRef, input: abi.encode(calls)}); + } + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), 0); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), _minAmountOut); + + _wrapTokenFromGenericCallForwarder({token: _tokenB, amount: _minAmountOut}); + } + + /// @notice Swaps where the DEX router pulls the input token via a Permit2 signature + /// (`ISignatureTransfer.permitTransferFrom`). The owner is the generic call forwarder, whose ERC-1271 + /// implementation accepts any signature, so an empty signature is passed. + function test_calls_allow_to_swap_erc20s_on_a_dex_with_permit2_signature() public { + _unwrapTokenIntoGenericCallForwarder({token: _tokenA, amount: _TRANSFER_AMOUNT}); + + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), _TRANSFER_AMOUNT); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), 0); + { + // Approve Permit2 to spend tokenA, swap it for tokenB pulling via a Permit2 signature, and approve Permit2 + // to pull tokenB for the subsequent wrap. + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(_tokenA), amount: _TRANSFER_AMOUNT}), + nonce: 789, + deadline: _swapDeadline() + }); + + GenericCallForwarder.Call[] memory calls = new GenericCallForwarder.Call[](3); + calls[0] = GenericCallForwarder.Call({ + to: address(_tokenA), + value: 0, + data: abi.encodeCall(IERC20.approve, (address(_permit2), _TRANSFER_AMOUNT)) + }); + calls[1] = GenericCallForwarder.Call({ + to: address(_dexRouter), + value: 0, + data: abi.encodeCall( + DexRouterMock.swapExactTokensForTokensWithPermit2Signature, + (_minAmountOut, address(_tokenB), address(_genericCallFwd), permit, "") + ) + }); + calls[2] = GenericCallForwarder.Call({ + to: address(_tokenB), value: 0, data: abi.encodeCall(IERC20.approve, (address(_permit2), _minAmountOut)) + }); + + vm.prank(_PROTOCOL_ADAPTER); + _genericCallFwd.forwardCall({logicRef: _genericCallResourceLogicRef, input: abi.encode(calls)}); + } + assertEq(_tokenA.balanceOf(address(_genericCallFwd)), 0); + assertEq(_tokenB.balanceOf(address(_genericCallFwd)), _minAmountOut); + + _wrapTokenFromGenericCallForwarder({token: _tokenB, amount: _minAmountOut}); + } + + /// @notice Unwraps `_TRANSFER_AMOUNT` of `token` from the ERC20 forwarder into the generic call forwarder + /// (triggered by an ERC20 resource) and returns the forwarder output. + function _unwrapTokenIntoGenericCallForwarder(ERC20Example token, uint256 amount) internal { + assertEq(token.balanceOf(address(_erc20Fwd)), amount); + + bytes memory unwrapInput = abi.encode( + ERC20Forwarder.CallType.Unwrap, + address(token), + amount, + ERC20Forwarder.UnwrapData({receiver: address(_genericCallFwd)}) + ); + + vm.prank(_PROTOCOL_ADAPTER); + _erc20Fwd.forwardCall({logicRef: _erc20ResourceLogicRef, input: unwrapInput}); + + assertEq(token.balanceOf(address(_erc20Fwd)), 0); + } + + /// @notice Wraps `amount` of `token` from the generic call forwarder into the ERC20 forwarder (triggered by an + /// ERC20 resource) and returns the forwarder output. + /// @dev The generic call forwarder implements ERC-1271 and always returns the magic value, so any `r, s, v` bytes + /// are valid. + function _wrapTokenFromGenericCallForwarder(ERC20Example token, uint128 amount) internal { + bytes memory wrapInput = abi.encode( + ERC20Forwarder.CallType.Wrap, + address(token), + amount, + ERC20Forwarder.WrapData({ + nonce: 456, + deadline: Time.timestamp() + 5 minutes, + owner: address(_genericCallFwd), + actionTreeRoot: _ACTION_TREE_ROOT, + r: bytes32(0), + s: bytes32(0), + v: 27 + }) + ); + + vm.prank(_PROTOCOL_ADAPTER); + _erc20Fwd.forwardCall({logicRef: _erc20ResourceLogicRef, input: wrapInput}); + } + + /// @notice The deadline shared by the swap calls. + function _swapDeadline() internal view returns (uint256 deadline) { + deadline = Time.timestamp() + 5 minutes; + } +} diff --git a/contracts/test/mocks/DexRouter.m.sol b/contracts/test/mocks/DexRouter.m.sol index 037529d..2b9eafe 100644 --- a/contracts/test/mocks/DexRouter.m.sol +++ b/contracts/test/mocks/DexRouter.m.sol @@ -8,25 +8,64 @@ import {SafeCast} from "@openzeppelin-contracts-5.6.1/utils/math/SafeCast.sol"; import { IAllowanceTransfer } from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/IAllowanceTransfer.sol"; +import { + ISignatureTransfer +} from "uniswap-permit2-0x000000000022D473030F116dDEE9F6B43aC78BA3/src/interfaces/ISignatureTransfer.sol"; contract DexRouterMock { using SafeERC20 for IERC20; - IAllowanceTransfer internal immutable _PERMIT2; + address internal immutable _PERMIT2; constructor(address permit2) { - _PERMIT2 = IAllowanceTransfer(permit2); + _PERMIT2 = permit2; + } + + /// @notice Swaps pulling the input token via a classic ERC-20 allowance (`approve` + `transferFrom`). + function swapExactTokensForTokensWithErc20Approval( + uint256 amountIn, + uint256 amountOutMin, + address tokenIn, + address tokenOut, + address to + ) external returns (uint256 amountOut) { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + amountOut = amountOutMin; + IERC20(tokenOut).safeTransfer(to, amountOut); } - function swapExactTokensForTokens( + /// @notice Swaps pulling the input token via a Permit2 allowance (`IAllowanceTransfer.approve` + `transferFrom`). + function swapExactTokensForTokensWithPermit2Allowance( uint256 amountIn, uint256 amountOutMin, - address[] calldata path, + address tokenIn, + address tokenOut, + address to + ) external returns (uint256 amountOut) { + IAllowanceTransfer(_PERMIT2).transferFrom(msg.sender, address(this), SafeCast.toUint160(amountIn), tokenIn); + amountOut = amountOutMin; + IERC20(tokenOut).safeTransfer(to, amountOut); + } + + /// @notice Swaps pulling the input token via a Permit2 signature (`ISignatureTransfer.permitTransferFrom`). + /// The input token is taken from `permit.permitted.token`. + function swapExactTokensForTokensWithPermit2Signature( + uint256 amountOutMin, + address tokenOut, address to, - uint256 /*deadline*/ + ISignatureTransfer.PermitTransferFrom calldata permit, + bytes calldata signature ) external returns (uint256 amountOut) { - _PERMIT2.transferFrom(msg.sender, address(this), SafeCast.toUint160(amountIn), path[0]); + ISignatureTransfer(_PERMIT2) + .permitTransferFrom({ + permit: permit, + transferDetails: ISignatureTransfer.SignatureTransferDetails({ + to: address(this), requestedAmount: permit.permitted.amount + }), + owner: msg.sender, + signature: signature + }); amountOut = amountOutMin; - IERC20(path[path.length - 1]).safeTransfer(to, amountOut); + IERC20(tokenOut).safeTransfer(to, amountOut); } }