From 7cb72e4689782189ade70e3e702775654a8fdf85 Mon Sep 17 00:00:00 2001 From: sanchaymittal Date: Mon, 1 May 2023 14:28:29 +0900 Subject: [PATCH] add: unit test --- forge-test/InstadappAdapter.t.sol | 162 ++++++++ forge-test/InstadappTarget.t.sol | 105 +++++ forge-test/interfaces/IBridgeToken.sol | 12 + forge-test/utils/OZERC20.sol | 513 +++++++++++++++++++++++++ forge-test/utils/TestERC20.sol | 50 +++ forge-test/utils/TestHelper.sol | 108 ++++++ 6 files changed, 950 insertions(+) create mode 100644 forge-test/InstadappAdapter.t.sol create mode 100644 forge-test/InstadappTarget.t.sol create mode 100644 forge-test/interfaces/IBridgeToken.sol create mode 100644 forge-test/utils/OZERC20.sol create mode 100644 forge-test/utils/TestERC20.sol create mode 100644 forge-test/utils/TestHelper.sol diff --git a/forge-test/InstadappAdapter.t.sol b/forge-test/InstadappAdapter.t.sol new file mode 100644 index 0000000..42b483b --- /dev/null +++ b/forge-test/InstadappAdapter.t.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {IDSA} from "../contracts/interfaces/IDSA.sol"; +import {InstadappAdapter} from "../contracts/InstadappAdapter.sol"; +import {TestHelper} from "./utils/TestHelper.sol"; + +contract MockInstadappReceiver is InstadappAdapter { + constructor() {} + + function tryAuthCast( + address dsaAddress, + address auth, + bytes memory signature, + CastData memory castData, + bytes32 salt, + uint256 deadline + ) external payable { + authCast(dsaAddress, auth, signature, castData, salt, deadline); + } +} + +contract InstadappAdapterTest is TestHelper { + // ============ Storage ============ + address dsa = address(1); + MockInstadappReceiver instadappReceiver; + uint256 deadline = block.timestamp + 3600 seconds; + + // ============ Test set up ============ + function setUp() public override { + super.setUp(); + instadappReceiver = new MockInstadappReceiver(); + } + + // ============ Utils ============ + function utils_dsaMocks(bool isAuth) public { + vm.mockCall(dsa, abi.encodeWithSelector(IDSA.isAuth.selector), abi.encode(isAuth)); + vm.mockCall(dsa, abi.encodeWithSelector(IDSA.cast.selector), abi.encode(bytes32(abi.encode(1)))); + } + + // ============ InstadappAdapter.authCast ============ + function test_InstadappAdapter__authCast_shouldRevertIfInvalidAuth() public { + utils_dsaMocks(false); + + address originSender = address(0x123); + + string[] memory _targetNames = new string[](2); + _targetNames[0] = "target1"; + _targetNames[1] = "target2"; + bytes[] memory _datas = new bytes[](2); + _datas[0] = bytes("data1"); + _datas[1] = bytes("data2"); + address _origin = originSender; + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + + bytes memory signature = bytes("0x111"); + address auth = originSender; + bytes32 salt = bytes32(abi.encode(1)); + + vm.expectRevert(bytes("Invalid Auth")); + instadappReceiver.tryAuthCast(dsa, auth, signature, castData, salt, deadline); + } + + function test_InstadappAdapter__authCast_shouldRevertIfInvalidSignature() public { + utils_dsaMocks(true); + + address originSender = address(0x123); + + string[] memory _targetNames = new string[](2); + _targetNames[0] = "target1"; + _targetNames[1] = "target2"; + bytes[] memory _datas = new bytes[](2); + _datas[0] = bytes("data1"); + _datas[1] = bytes("data2"); + address _origin = originSender; + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + + bytes + memory signature = hex"e91f49cb8bf236eafb590ba328a6ca75f4d189fa51bfce2ac774541801c17d3f2d3df798f18c0520db5a98d33362d507f890d5904c2aea1dd059a9b0f05fb3ad1c"; + + address auth = originSender; + bytes32 salt = bytes32(abi.encode(1)); + vm.expectRevert(bytes("Invalid signature")); + instadappReceiver.tryAuthCast(dsa, auth, signature, castData, salt, deadline); + } + + function test_InstadappAdapter__authCast_shouldWork() public { + utils_dsaMocks(true); + + address originSender = address(0xc1aAED5D41a3c3c33B1978EA55916f9A3EE1B397); + + string[] memory _targetNames = new string[](3); + _targetNames[0] = "target111"; + _targetNames[1] = "target222"; + _targetNames[2] = "target333"; + bytes[] memory _datas = new bytes[](3); + _datas[0] = bytes("0x111"); + _datas[1] = bytes("0x222"); + _datas[2] = bytes("0x333"); + address _origin = originSender; + bytes32 salt = bytes32(abi.encode(1)); + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + + bytes + memory signature = hex"e06eb18ed5fa1258094a9af413275fc057cb5139b4e48c979a7ef9d028e8748e39bfa2ea23722f296a07ae7a2d2fee26c7de3ad067a2c569819bec0fc3c9f0f51b"; + + address auth = originSender; + instadappReceiver.tryAuthCast{value: 1}(dsa, auth, signature, castData, salt, deadline); + } + + // ============ InstadappAdapter.verify ============ + function test_InstadappAdapter__verify_shouldReturnTrue() public { + utils_dsaMocks(true); + + address originSender = address(0xc1aAED5D41a3c3c33B1978EA55916f9A3EE1B397); + + string[] memory _targetNames = new string[](3); + _targetNames[0] = "target111"; + _targetNames[1] = "target222"; + _targetNames[2] = "target333"; + bytes[] memory _datas = new bytes[](3); + _datas[0] = bytes("0x111"); + _datas[1] = bytes("0x222"); + _datas[2] = bytes("0x333"); + address _origin = originSender; + bytes32 salt = bytes32(abi.encode(1)); + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + + bytes + memory signature = hex"e06eb18ed5fa1258094a9af413275fc057cb5139b4e48c979a7ef9d028e8748e39bfa2ea23722f296a07ae7a2d2fee26c7de3ad067a2c569819bec0fc3c9f0f51b"; + + address auth = originSender; + assertEq(instadappReceiver.verify(auth, signature, castData, salt, deadline), true); + } + + function test_InstadappAdapter__verify_shouldReturnFalse() public { + utils_dsaMocks(true); + + address originSender = address(0x123); + + string[] memory _targetNames = new string[](2); + _targetNames[0] = "target1"; + _targetNames[1] = "target2"; + bytes[] memory _datas = new bytes[](2); + _datas[0] = bytes("data1"); + _datas[1] = bytes("data2"); + address _origin = originSender; + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + + bytes + memory signature = hex"e91f49cb8bf236eafb590ba328a6ca75f4d189fa51bfce2ac774541801c17d3f2d3df798f18c0520db5a98d33362d507f890d5904c2aea1dd059a9b0f05fb3ad1c"; + + address auth = originSender; + bytes32 salt = bytes32(abi.encode(1)); + assertEq(instadappReceiver.verify(auth, signature, castData, salt, deadline), false); + } +} diff --git a/forge-test/InstadappTarget.t.sol b/forge-test/InstadappTarget.t.sol new file mode 100644 index 0000000..45afdf3 --- /dev/null +++ b/forge-test/InstadappTarget.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import {IDSA} from "../contracts/interfaces/IDSA.sol"; +import {InstadappAdapter} from "../contracts/InstadappAdapter.sol"; +import {TestHelper} from "./utils/TestHelper.sol"; +import {InstadappTarget} from "../contracts/InstadappTarget.sol"; +import {TestERC20} from "./utils/TestERC20.sol"; + +contract InstadappTargetTest is TestHelper, EIP712 { + // ============ Storage ============ + InstadappTarget instadappTarget; + + // ============ Events ============ + event AuthCast(bytes32 transferId, address dsaAddress, address auth, bool success, bytes returnedData); + + // ============ Test set up ============ + function setUp() public override { + super.setUp(); + + instadappTarget = new InstadappTarget(MOCK_CONNEXT); + } + + constructor() EIP712("InstaTargetAuth", "1") {} + + function test_InstadappTarget__xReceive_shouldRevertIfCallerNotConnext() public { + bytes32 transferId = keccak256(abi.encode(0x123)); + uint256 amount = 1 ether; + address asset = address(0x123123123); + bytes memory callData = bytes("123"); + + vm.prank(address(0x456)); + vm.expectRevert(bytes("Caller must be Connext")); + instadappTarget.xReceive(transferId, amount, asset, address(0), 0, callData); + } + + function test_InstadappTarget__xReceive_shouldRevertIfFallbackAddressInvalid() public { + // Mock xReceive data + bytes32 transferId = keccak256(abi.encode(0x123)); + uint256 amount = 1 ether; + address asset = address(0x123123123); + + // Mock callData of `xReceive` + address originSender = address(0xc1aAED5D41a3c3c33B1978EA55916f9A3EE1B397); + string[] memory _targetNames = new string[](3); + _targetNames[0] = "target111"; + _targetNames[1] = "target222"; + _targetNames[2] = "target333"; + bytes[] memory _datas = new bytes[](3); + _datas[0] = bytes("0x111"); + _datas[1] = bytes("0x222"); + _datas[2] = bytes("0x333"); + address _origin = originSender; + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + bytes + memory signature = hex"d75642b5e0cfceac682011943f3586fefc3709594a89bf8087acc58d2009d85412aca8b1f9b63989de45da85f5ffcea52cc5077a61a2128fa7322a97523afe0e1b"; + address auth = originSender; + bytes memory callData = abi.encode(address(0), auth, signature, castData); + + vm.prank(MOCK_CONNEXT); + vm.expectRevert(bytes("!invalidFallback")); + instadappTarget.xReceive(transferId, amount, asset, address(0), 0, callData); + } + + function test_InstadappTarget__xReceive_shouldWork() public { + // Mock xReceive data + bytes32 transferId = keccak256(abi.encode(0x123)); + uint256 amount = 1 ether; + TestERC20 asset = new TestERC20("Test", "TST"); + + // Mock callData of `xReceive` + address originSender = vm.addr(1); + string[] memory _targetNames = new string[](3); + _targetNames[0] = "target111"; + _targetNames[1] = "target222"; + _targetNames[2] = "target333"; + bytes[] memory _datas = new bytes[](3); + _datas[0] = bytes("0x111"); + _datas[1] = bytes("0x222"); + _datas[2] = bytes("0x333"); + address _origin = originSender; + address dsa = address(0x111222333); + bytes32 salt = bytes32(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef); + + InstadappAdapter.CastData memory castData = InstadappAdapter.CastData(_targetNames, _datas, _origin); + bytes32 digest = _hashTypedDataV4( + keccak256(abi.encode(instadappTarget.SIG_TYPEHASH, instadappTarget.hash(castData), salt)) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest); + bytes memory signature = abi.encodePacked(r, s, v); + address auth = originSender; + bytes memory callData = abi.encode(dsa, auth, signature, castData); + + bytes memory returnedData = hex""; + vm.expectEmit(true, false, false, true); + emit AuthCast(transferId, dsa, auth, false, returnedData); + deal(address(asset), address(instadappTarget), amount); + vm.prank(MOCK_CONNEXT); + instadappTarget.xReceive(transferId, amount, address(asset), address(0), 0, callData); + } +} diff --git a/forge-test/interfaces/IBridgeToken.sol b/forge-test/interfaces/IBridgeToken.sol new file mode 100644 index 0000000..46a6e0e --- /dev/null +++ b/forge-test/interfaces/IBridgeToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.17; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +interface IBridgeToken is IERC20Metadata { + function burn(address _from, uint256 _amnt) external; + + function mint(address _to, uint256 _amnt) external; + + function setDetails(string calldata _name, string calldata _symbol) external; +} diff --git a/forge-test/utils/OZERC20.sol b/forge-test/utils/OZERC20.sol new file mode 100644 index 0000000..135e3b7 --- /dev/null +++ b/forge-test/utils/OZERC20.sol @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// This is modified from "@openzeppelin/contracts/token/ERC20/IERC20.sol" +// Modifications were made to allow the name, hashed name, and cached +// domain separator to be internal + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * Implements ERC20 Permit extension allowing approvals to be made via + * signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 + * allowance (see {IERC20-allowance}) by presenting a message signed by the + * account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + * + * @dev Cannot use default ERC20/ERC20Permit implementation as there is no way to update + * the name (set to private). + * + * Cannot use default EIP712 implementation as the _HASHED_NAME may change. + * These functions use the same implementation, with easier storage access. + */ +contract ERC20 is IERC20Metadata, IERC20Permit { + // See ERC20 + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string internal _name; // made internal, need access + string internal _symbol; // made internal, need access + uint8 internal _decimals; // made internal, need access + + // See ERC20Permit + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // See EIP712 + // Immutables used in EIP 712 structured data hashing & signing + // https://eips.ethereum.org/EIPS/eip-712 + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 internal constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + // made internal, need access + + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 internal _CACHED_DOMAIN_SEPARATOR; // made internal, may change + uint256 private immutable _CACHED_CHAIN_ID; + address private immutable _CACHED_THIS; + + bytes32 internal _HASHED_NAME; // made internal, may change + bytes32 internal immutable _HASHED_VERSION; // made internal, need access + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, + * and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC20 token name. + */ + constructor(uint8 decimals_, string memory name_, string memory symbol_, string memory version_) { + // ERC20 + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + + // EIP712 + bytes32 hashedName = keccak256(bytes(name_)); + bytes32 hashedVersion = keccak256(bytes(version_)); + _HASHED_NAME = hashedName; + _HASHED_VERSION = hashedVersion; + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, hashedName, hashedVersion); + _CACHED_THIS = address(this); + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless this function is + * overridden; + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + _transfer(msg.sender, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address _owner, address _spender) public view virtual override returns (uint256) { + return _allowances[_owner][_spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `_sender` and `recipient` cannot be the zero address. + * - `_sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``_sender``'s tokens of at least + * `amount`. + */ + function transferFrom(address _sender, address _recipient, uint256 _amount) public virtual override returns (bool) { + _spendAllowance(_sender, msg.sender, _amount); + _transfer(_sender, _recipient, _amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `_spender` cannot be the zero address. + */ + function increaseAllowance(address _spender, uint256 _addedValue) public virtual returns (bool) { + _approve(msg.sender, _spender, _allowances[msg.sender][_spender] + _addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `_spender` cannot be the zero address. + * - `_spender` must have allowance for the caller of at least + * `_subtractedValue`. + */ + function decreaseAllowance(address _spender, uint256 _subtractedValue) public virtual returns (bool) { + uint256 currentAllowance = allowance(msg.sender, _spender); + require(currentAllowance >= _subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(msg.sender, _spender, currentAllowance - _subtractedValue); + } + + return true; + } + + /** + * @dev Moves tokens `amount` from `_sender` to `_recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `_sender` cannot be the zero address. + * - `_recipient` cannot be the zero address. + * - `_sender` must have a balance of at least `amount`. + */ + function _transfer(address _sender, address _recipient, uint256 _amount) internal virtual { + require(_sender != address(0), "ERC20: transfer from the zero address"); + require(_recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(_sender, _recipient, _amount); + + uint256 fromBalance = _balances[_sender]; + require(fromBalance >= _amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[_sender] = fromBalance - _amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[_recipient] += _amount; + } + + emit Transfer(_sender, _recipient, _amount); + + _afterTokenTransfer(_sender, _recipient, _amount); + } + + /** @dev Creates `_amount` tokens and assigns them to `_account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `to` cannot be the zero address. + */ + function _mint(address _account, uint256 _amount) internal virtual { + require(_account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), _account, _amount); + + _totalSupply += _amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[_account] += _amount; + } + emit Transfer(address(0), _account, _amount); + + _afterTokenTransfer(address(0), _account, _amount); + } + + /** + * @dev Destroys `_amount` tokens from `_account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `_account` cannot be the zero address. + * - `_account` must have at least `_amount` tokens. + */ + function _burn(address _account, uint256 _amount) internal virtual { + require(_account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(_account, address(0), _amount); + + uint256 accountBalance = _balances[_account]; + require(accountBalance >= _amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[_account] = accountBalance - _amount; + // Overflow not possible: amount <= accountBalance <= totalSupply + _totalSupply -= _amount; + } + + emit Transfer(_account, address(0), _amount); + + _afterTokenTransfer(_account, address(0), _amount); + } + + /** + * @dev Sets `_amount` as the allowance of `_spender` over the `_owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `_owner` cannot be the zero address. + * - `_spender` cannot be the zero address. + */ + function _approve(address _owner, address _spender, uint256 _amount) internal virtual { + require(_owner != address(0), "ERC20: approve from the zero address"); + require(_spender != address(0), "ERC20: approve to the zero address"); + + _allowances[_owner][_spender] = _amount; + emit Approval(_owner, _spender, _amount); + } + + /** + * @dev Updates `_owner` s allowance for `_spender` based on spent `_amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address _owner, address _spender, uint256 _amount) internal virtual { + uint256 currentAllowance = allowance(_owner, _spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= _amount, "ERC20: insufficient allowance"); + unchecked { + _approve(_owner, _spender, currentAllowance - _amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `_from` and `_to` are both non-zero, `_amount` of ``_from``'s tokens + * will be to transferred to `_to`. + * - when `_from` is zero, `_amount` tokens will be minted for `_to`. + * - when `_to` is zero, `_amount` of ``_from``'s tokens will be burned. + * - `_from` and `_to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address _from, address _to, uint256 _amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address _from, address _to, uint256 _amount) internal virtual {} + + /** + * @dev See {IERC20Permit-permit}. + * @notice Sets approval from owner to spender to value + * as long as deadline has not passed + * by submitting a valid signature from owner + * Uses EIP 712 structured data hashing & signing + * https://eips.ethereum.org/EIPS/eip-712 + * @param _owner The account setting approval & signing the message + * @param _spender The account receiving approval to spend owner's tokens + * @param _value The amount to set approval for + * @param _deadline The timestamp before which the signature must be submitted + * @param _v ECDSA signature v + * @param _r ECDSA signature r + * @param _s ECDSA signature s + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public virtual override { + require(block.timestamp <= _deadline, "ERC20Permit: expired deadline"); + + bytes32 _structHash = keccak256( + abi.encode(_PERMIT_TYPEHASH, _owner, _spender, _value, _useNonce(_owner), _deadline) + ); + + bytes32 _hash = _hashTypedDataV4(_structHash); + + address _signer = ECDSA.recover(_hash, _v, _r, _s); + require(_signer == _owner, "ERC20Permit: invalid signature"); + + _approve(_owner, _spender, _value); + } + + /** + * @dev See {IERC20Permit-nonces}. + */ + function nonces(address _owner) public view virtual override returns (uint256) { + return _nonces[_owner].current(); + } + + /** + * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. + * This is ALWAYS calculated at runtime because the token name is mutable, not constant. + */ + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * @dev See {EIP712._buildDomainSeparator} + */ + function _useNonce(address _owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[_owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns the domain separator for the current chain. + * @dev See {EIP712._buildDomainSeparator} + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _CACHED_THIS && block.chainid == _CACHED_CHAIN_ID) { + return _CACHED_DOMAIN_SEPARATOR; + } else { + return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + } + } + + /** + * @dev See {EIP712._buildDomainSeparator}. Made internal to allow usage in parent class. + */ + function _buildDomainSeparator( + bytes32 typeHash, + bytes32 nameHash, + bytes32 versionHash + ) internal view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); + } +} diff --git a/forge-test/utils/TestERC20.sol b/forge-test/utils/TestERC20.sol new file mode 100644 index 0000000..f001cbe --- /dev/null +++ b/forge-test/utils/TestERC20.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {ERC20} from "./OZERC20.sol"; +import {IERC20Metadata, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IBridgeToken} from "../interfaces/IBridgeToken.sol"; + +/** + * @notice This token is ONLY useful for testing + * @dev Anybody can mint as many tokens as they like + * @dev Anybody can burn anyone else's tokens + */ +contract TestERC20 is ERC20, IBridgeToken { + constructor(string memory _name, string memory _symbol) ERC20(18, _name, _symbol, "1") { + _mint(msg.sender, 1000000 ether); + } + + // ============ Bridge functions =============== + function setDetails(string calldata _newName, string calldata _newSymbol) external override { + // Does nothing, in practice will update the details to match the hash in message + // not the autodeployed results + _name = _newName; + _symbol = _newSymbol; + } + + // ============ Token functions =============== + function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) { + return ERC20.balanceOf(account); + } + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + function symbol() public view override(ERC20, IERC20Metadata) returns (string memory) { + return ERC20.symbol(); + } + + function name() public view override(ERC20, IERC20Metadata) returns (string memory) { + return ERC20.name(); + } + + function decimals() public view override(ERC20, IERC20Metadata) returns (uint8) { + return ERC20.decimals(); + } +} diff --git a/forge-test/utils/TestHelper.sol b/forge-test/utils/TestHelper.sol new file mode 100644 index 0000000..876d8c5 --- /dev/null +++ b/forge-test/utils/TestHelper.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import "forge-std/Test.sol"; + +contract TestHelper is Test { + /// Testnet Domain IDs + uint32 public GOERLI_DOMAIN_ID = 1735353714; + uint32 public OPTIMISM_GOERLI_DOMAIN_ID = 1735356532; + uint32 public ARBITRUM_GOERLI_DOMAIN_ID = 1734439522; + uint32 public POLYGON_MUMBAI_DOMAIN_ID = 9991; + + /// Testnet Chain IDs + uint32 public GOERLI_CHAIN_ID = 5; + uint32 public OPTIMISM_GOERLI_CHAIN_ID = 420; + uint32 public ARBITRUM_GOERLI_CHAIN_ID = 421613; + uint32 public POLYGON_MUMBAI_CHAIN_ID = 80001; + + /// Mainnet Domain IDs + uint32 public ARBITRUM_DOMAIN_ID = 1634886255; + uint32 public OPTIMISM_DOMAIN_ID = 1869640809; + uint32 public BNB_DOMAIN_ID = 6450786; + uint32 public POLYGON_DOMAIN_ID = 1886350457; + + /// Mainnet Chain IDs + uint32 public ARBITRUM_CHAIN_ID = 42161; + uint32 public OPTIMISM_CHAIN_ID = 10; + + // Live Addresses + address public CONNEXT_ARBITRUM = 0xEE9deC2712cCE65174B561151701Bf54b99C24C8; + address public CONNEXT_OPTIMISM = 0x8f7492DE823025b4CfaAB1D34c58963F2af5DEDA; + address public CONNEXT_BNB = 0xCd401c10afa37d641d2F594852DA94C700e4F2CE; + address public CONNEXT_POLYGON = 0x11984dc4465481512eb5b777E44061C158CF2259; + + // Forks + uint256 public arbitrumForkId; + uint256 public optimismForkId; + uint256 public bnbForkId; + uint256 public polygonForkId; + + /// Mock Addresses + address public USER_CHAIN_A = address(bytes20(keccak256("USER_CHAIN_A"))); + address public USER_CHAIN_B = address(bytes20(keccak256("USER_CHAIN_B"))); + address public MOCK_CONNEXT = address(bytes20(keccak256("MOCK_CONNEXT"))); + address public MOCK_MEAN_FINANCE = address(bytes20(keccak256("MOCK_MEAN_FINANCE"))); + address public TokenA_ERC20 = address(bytes20(keccak256("TokenA_ERC20"))); + address public TokenB_ERC20 = address(bytes20(keccak256("TokenB_ERC20"))); + + // OneInch Aggregator constants + uint256 public constant ONE_FOR_ZERO_MASK = 1 << 255; + + function setUp() public virtual { + vm.label(MOCK_CONNEXT, "Mock Connext"); + vm.label(MOCK_MEAN_FINANCE, "Mock Mean Finance"); + vm.label(TokenA_ERC20, "TokenA_ERC20"); + vm.label(TokenB_ERC20, "TokenB_ERC20"); + vm.label(USER_CHAIN_A, "User Chain A"); + vm.label(USER_CHAIN_B, "User Chain B"); + } + + function setUpArbitrum(uint256 blockNumber) public { + arbitrumForkId = vm.createSelectFork(getRpc(42161), blockNumber); + vm.label(CONNEXT_ARBITRUM, "Connext Arbitrum"); + } + + function setUpOptimism(uint256 blockNumber) public { + optimismForkId = vm.createSelectFork(getRpc(10), blockNumber); + vm.label(CONNEXT_OPTIMISM, "Connext Optimism"); + } + + function setUpBNB(uint256 blockNumber) public { + bnbForkId = vm.createSelectFork(getRpc(56), blockNumber); + vm.label(CONNEXT_BNB, "Connext BNB"); + } + + function setUpPolygon(uint256 blockNumber) public { + polygonForkId = vm.createSelectFork(getRpc(137), blockNumber); + vm.label(CONNEXT_POLYGON, "Connext Polygon"); + } + + function getRpc(uint256 chainId) internal view returns (string memory) { + string memory keyName; + string memory defaultRpc; + + if (chainId == 1) { + keyName = "MAINNET_RPC_URL"; + defaultRpc = "https://eth.llamarpc.com"; + } else if (chainId == 10) { + keyName = "OPTIMISM_RPC_URL"; + defaultRpc = "https://mainnet.optimism.io"; + } else if (chainId == 42161) { + keyName = "ARBITRUM_RPC_URL"; + defaultRpc = "https://arb1.arbitrum.io/rpc"; + } else if (chainId == 56) { + keyName = "BNB_RPC_URL"; + defaultRpc = "https://bsc-dataseed.binance.org"; + } else if (chainId == 137) { + keyName = "POLYGON_RPC_URL"; + defaultRpc = "https://polygon.llamarpc.com"; + } + + try vm.envString(keyName) { + return vm.envString(keyName); + } catch { + return defaultRpc; + } + } +}