Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions contracts/test/GenericCallForwarder.integration.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// 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);
}
}

Loading
Loading