The home for Hyperlane core contracts, sdk packages, and other infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hyperlane-monorepo/solidity/test/token/HypERC20.t.sol

364 lines
11 KiB

// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.13;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
import "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {ERC20Test} from "../../contracts/test/ERC20Test.sol";
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol";
import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol";
import {GasRouter} from "../../contracts/client/GasRouter.sol";
import {HypERC20} from "../../contracts/token/HypERC20.sol";
import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol";
import {HypNative} from "../../contracts/token/HypNative.sol";
import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol";
abstract contract HypTokenTest is Test {
using TypeCasts for address;
uint32 internal constant ORIGIN = 11;
uint32 internal constant DESTINATION = 12;
uint8 internal constant DECIMALS = 18;
uint256 internal constant TOTAL_SUPPLY = 1_000_000e18;
uint256 internal REQUIRED_VALUE; // initialized in setUp
uint256 internal constant GAS_LIMIT = 10_000;
uint256 internal constant TRANSFER_AMT = 100e18;
string internal constant NAME = "HyperlaneInu";
string internal constant SYMBOL = "HYP";
address internal constant ALICE = address(0x1);
address internal constant BOB = address(0x2);
ERC20Test internal primaryToken;
TokenRouter internal localToken;
HypERC20 internal remoteToken;
TestMailbox internal localMailbox;
TestMailbox internal remoteMailbox;
TestPostDispatchHook internal noopHook;
TestInterchainGasPaymaster internal igp;
event SentTransferRemote(
uint32 indexed destination,
bytes32 indexed recipient,
uint256 amount
);
event ReceivedTransferRemote(
uint32 indexed origin,
bytes32 indexed recipient,
uint256 amount
);
function setUp() public virtual {
localMailbox = new TestMailbox(ORIGIN);
remoteMailbox = new TestMailbox(DESTINATION);
primaryToken = new ERC20Test(NAME, SYMBOL, TOTAL_SUPPLY, DECIMALS);
noopHook = new TestPostDispatchHook();
localMailbox.setDefaultHook(address(noopHook));
localMailbox.setRequiredHook(address(noopHook));
REQUIRED_VALUE = noopHook.quoteDispatch("", "");
remoteToken = new HypERC20(DECIMALS, address(remoteMailbox));
remoteToken.initialize(TOTAL_SUPPLY, NAME, SYMBOL);
remoteToken.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);
igp = new TestInterchainGasPaymaster();
vm.deal(ALICE, 125000);
}
function _enrollRemoteTokenRouter() internal {
remoteToken.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);
}
function _expectRemoteBalance(address _user, uint256 _balance) internal {
assertEq(remoteToken.balanceOf(_user), _balance);
}
function _processTransfers(address _recipient, uint256 _amount) internal {
vm.prank(address(remoteMailbox));
remoteToken.handle(
ORIGIN,
address(localToken).addressToBytes32(),
abi.encodePacked(_recipient.addressToBytes32(), _amount)
);
}
function _handleLocalTransfer(uint256 _transferAmount) internal {
vm.prank(address(localMailbox));
localToken.handle(
DESTINATION,
address(remoteToken).addressToBytes32(),
abi.encodePacked(ALICE.addressToBytes32(), _transferAmount)
);
}
function _mintAndApprove(uint256 _amount, address _account) internal {
primaryToken.mint(_amount);
primaryToken.approve(_account, _amount);
}
function _setCustomGasConfig() internal {
localToken.setHook(address(igp));
TokenRouter.GasRouterConfig[]
memory config = new TokenRouter.GasRouterConfig[](1);
config[0] = GasRouter.GasRouterConfig({
domain: DESTINATION,
gas: GAS_LIMIT
});
localToken.setDestinationGas(config);
}
function _performRemoteTransfer(
uint256 _msgValue,
uint256 _amount
) internal {
vm.prank(ALICE);
localToken.transferRemote{value: _msgValue}(
DESTINATION,
BOB.addressToBytes32(),
_amount
);
vm.expectEmit(true, true, false, true);
emit ReceivedTransferRemote(ORIGIN, BOB.addressToBytes32(), _amount);
_processTransfers(BOB, _amount);
assertEq(remoteToken.balanceOf(BOB), _amount);
}
function _performRemoteTransferAndGas(
uint256 _msgValue,
uint256 _amount,
uint256 _gasOverhead
) internal {
uint256 ethBalance = ALICE.balance;
_performRemoteTransfer(_msgValue + _gasOverhead, _amount);
assertEq(ALICE.balance, ethBalance - REQUIRED_VALUE - _gasOverhead);
}
function _performRemoteTransferWithEmit(
uint256 _msgValue,
uint256 _amount,
uint256 _gasOverhead
) internal {
vm.expectEmit(true, true, false, true);
emit SentTransferRemote(DESTINATION, BOB.addressToBytes32(), _amount);
_performRemoteTransferAndGas(_msgValue, _amount, _gasOverhead);
}
function testBenchmark_overheadGasUsage() public virtual {
vm.prank(address(localMailbox));
uint256 gasBefore = gasleft();
localToken.handle(
DESTINATION,
address(remoteToken).addressToBytes32(),
abi.encodePacked(BOB.addressToBytes32(), TRANSFER_AMT)
);
uint256 gasAfter = gasleft();
console.log("Overhead gas usage: %d", gasBefore - gasAfter);
}
}
contract HypERC20Test is HypTokenTest {
using TypeCasts for address;
HypERC20 internal erc20Token;
function setUp() public override {
super.setUp();
localToken = new HypERC20(DECIMALS, address(localMailbox));
erc20Token = HypERC20(address(localToken));
erc20Token.initialize(TOTAL_SUPPLY, NAME, SYMBOL);
erc20Token.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
erc20Token.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testInitialize_revert_ifAlreadyInitialized() public {
vm.expectRevert("Initializable: contract is already initialized");
erc20Token.initialize(TOTAL_SUPPLY, NAME, SYMBOL);
}
function testTotalSupply() public {
assertEq(erc20Token.totalSupply(), TOTAL_SUPPLY);
}
function testDecimals() public {
assertEq(erc20Token.decimals(), DECIMALS);
}
function testLocalTransfers() public {
assertEq(erc20Token.balanceOf(ALICE), 1000e18);
assertEq(erc20Token.balanceOf(BOB), 0);
vm.prank(ALICE);
erc20Token.transfer(BOB, 100e18);
assertEq(erc20Token.balanceOf(ALICE), 900e18);
assertEq(erc20Token.balanceOf(BOB), 100e18);
}
function testRemoteTransfer() public {
remoteToken.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);
uint256 balanceBefore = erc20Token.balanceOf(ALICE);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(erc20Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testRemoteTransfer_invalidAmount() public {
vm.expectRevert("ERC20: burn amount exceeds balance");
_performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMT * 11);
assertEq(erc20Token.balanceOf(ALICE), 1000e18);
}
function testRemoteTransfer_withCustomGasConfig() public {
_setCustomGasConfig();
uint256 balanceBefore = erc20Token.balanceOf(ALICE);
_performRemoteTransferAndGas(
REQUIRED_VALUE,
TRANSFER_AMT,
GAS_LIMIT * igp.gasPrice()
);
assertEq(erc20Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
}
contract HypERC20CollateralTest is HypTokenTest {
using TypeCasts for address;
HypERC20Collateral internal erc20Collateral;
function setUp() public override {
super.setUp();
localToken = new HypERC20Collateral(
address(primaryToken),
address(localMailbox)
);
erc20Collateral = HypERC20Collateral(address(localToken));
erc20Collateral.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
primaryToken.transfer(address(localToken), 1000e18);
primaryToken.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testInitialize_revert_ifAlreadyInitialized() public {}
function testRemoteTransfer() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testRemoteTransfer_invalidAllowance() public {
vm.expectRevert("ERC20: insufficient allowance");
_performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMT);
assertEq(localToken.balanceOf(ALICE), 1000e18);
}
function testRemoteTransfer_withCustomGasConfig() public {
_setCustomGasConfig();
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
_performRemoteTransferAndGas(
REQUIRED_VALUE,
TRANSFER_AMT,
GAS_LIMIT * igp.gasPrice()
);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
}
contract HypNativeTest is HypTokenTest {
using TypeCasts for address;
HypNative internal nativeToken;
function setUp() public override {
super.setUp();
localToken = new HypNative(address(localMailbox));
nativeToken = HypNative(payable(address(localToken)));
nativeToken.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
vm.deal(address(localToken), 1000e18);
vm.deal(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
function testInitialize_revert_ifAlreadyInitialized() public {}
function testRemoteTransfer() public {
_performRemoteTransferWithEmit(
REQUIRED_VALUE,
TRANSFER_AMT,
TRANSFER_AMT
);
}
function testRemoteTransfer_invalidAmount() public {
vm.expectRevert("Native: amount exceeds msg.value");
_performRemoteTransfer(
REQUIRED_VALUE + TRANSFER_AMT,
TRANSFER_AMT * 10
);
assertEq(localToken.balanceOf(ALICE), 1000e18);
}
function testRemoteTransfer_withCustomGasConfig() public {
_setCustomGasConfig();
_performRemoteTransferAndGas(
REQUIRED_VALUE,
TRANSFER_AMT,
TRANSFER_AMT + GAS_LIMIT * igp.gasPrice()
);
}
}