From 359d50894d59212f6fafbae7d24fb0a396baeb76 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Tue, 30 Jul 2024 15:23:59 +0200 Subject: [PATCH 01/14] feat: permissionless token addition with GMP --- contracts/XERC20/XERC20.sol | 286 ++++++++++++++ contracts/XERC20/XERC20Factory.sol | 142 +++++++ contracts/XERC20/XERC20Lockbox.sol | 160 ++++++++ contracts/XERC20/interfaces/IXERC20.sol | 128 ++++++ .../XERC20/interfaces/IXERC20Factory.sol | 77 ++++ .../XERC20/interfaces/IXERC20Lockbox.sol | 85 ++++ contracts/adapters/GmpTransferAdapter.sol | 114 ++++++ contracts/interfaces/IBridge.sol | 24 +- package.json | 3 +- test/gmpTransferAdapter/collectFee.js | 186 +++++++++ .../erc20Transfer/deposit.js | 201 ++++++++++ .../erc20Transfer/executeProposal.js | 372 ++++++++++++++++++ .../nativeTransfer/deposit.js | 185 +++++++++ .../nativeTransfer/executeProposal.js | 359 +++++++++++++++++ yarn.lock | 5 + 15 files changed, 2325 insertions(+), 2 deletions(-) create mode 100644 contracts/XERC20/XERC20.sol create mode 100644 contracts/XERC20/XERC20Factory.sol create mode 100644 contracts/XERC20/XERC20Lockbox.sol create mode 100644 contracts/XERC20/interfaces/IXERC20.sol create mode 100644 contracts/XERC20/interfaces/IXERC20Factory.sol create mode 100644 contracts/XERC20/interfaces/IXERC20Lockbox.sol create mode 100644 contracts/adapters/GmpTransferAdapter.sol create mode 100644 test/gmpTransferAdapter/collectFee.js create mode 100644 test/gmpTransferAdapter/erc20Transfer/deposit.js create mode 100644 test/gmpTransferAdapter/erc20Transfer/executeProposal.js create mode 100644 test/gmpTransferAdapter/nativeTransfer/deposit.js create mode 100644 test/gmpTransferAdapter/nativeTransfer/executeProposal.js diff --git a/contracts/XERC20/XERC20.sol b/contracts/XERC20/XERC20.sol new file mode 100644 index 00000000..c8deb99b --- /dev/null +++ b/contracts/XERC20/XERC20.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +import { IXERC20 } from "./interfaces/IXERC20.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @custom:attribution https://github.com/defi-wonderland/xERC20/blob/dev/solidity/contracts/XERC20.sol + */ +contract XERC20 is ERC20, Ownable, IXERC20, ERC20Permit { + /** + * @notice The duration it takes for the limits to fully replenish + */ + uint256 private constant _DURATION = 1 days; + + /** + * @notice The address of the factory which deployed this contract + */ + address public immutable FACTORY; + + /** + * @notice The address of the lockbox contract + */ + address public lockbox; + + /** + * @notice Maps bridge address to bridge configurations + */ + mapping(address => Bridge) public bridges; + + /** + * @notice Constructs the initial config of the XERC20 + * + * @param _name The name of the token + * @param _symbol The symbol of the token + * @param _factory The factory which deployed this contract + */ + constructor(string memory _name, string memory _symbol, address _factory) ERC20(_name, _symbol) ERC20Permit(_name) { + _transferOwnership(_factory); + FACTORY = _factory; + } + + /** + * @notice Mints tokens for a user + * @dev Can only be called by a bridge + * @param _user The address of the user who needs tokens minted + * @param _amount The amount of tokens being minted + */ + function mint(address _user, uint256 _amount) public { + _mintWithCaller(msg.sender, _user, _amount); + } + + /** + * @notice Burns tokens for a user + * @dev Can only be called by a bridge + * @param _user The address of the user who needs tokens burned + * @param _amount The amount of tokens being burned + */ + function burn(address _user, uint256 _amount) public { + if (msg.sender != _user) { + _spendAllowance(_user, msg.sender, _amount); + } + + _burnWithCaller(msg.sender, _user, _amount); + } + + /** + * @notice Sets the lockbox address + * + * @param _lockbox The address of the lockbox + */ + function setLockbox(address _lockbox) public { + if (msg.sender != FACTORY) revert IXERC20_NotFactory(); + lockbox = _lockbox; + + emit LockboxSet(_lockbox); + } + + /** + * @notice Updates the limits of any bridge + * @dev Can only be called by the owner + * @param _mintingLimit The updated minting limit we are setting to the bridge + * @param _burningLimit The updated burning limit we are setting to the bridge + * @param _bridge The address of the bridge we are setting the limits too + */ + function setLimits(address _bridge, uint256 _mintingLimit, uint256 _burningLimit) external onlyOwner { + if (_mintingLimit > (type(uint256).max / 2) || _burningLimit > (type(uint256).max / 2)) { + revert IXERC20_LimitsTooHigh(); + } + + _changeMinterLimit(_bridge, _mintingLimit); + _changeBurnerLimit(_bridge, _burningLimit); + emit BridgeLimitsSet(_mintingLimit, _burningLimit, _bridge); + } + + /** + * @notice Returns the max limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + function mintingMaxLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = bridges[_bridge].minterParams.maxLimit; + } + + /** + * @notice Returns the max limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + function burningMaxLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = bridges[_bridge].burnerParams.maxLimit; + } + + /** + * @notice Returns the current limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + function mintingCurrentLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = _getCurrentLimit( + bridges[_bridge].minterParams.currentLimit, + bridges[_bridge].minterParams.maxLimit, + bridges[_bridge].minterParams.timestamp, + bridges[_bridge].minterParams.ratePerSecond + ); + } + + /** + * @notice Returns the current limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + function burningCurrentLimitOf(address _bridge) public view returns (uint256 _limit) { + _limit = _getCurrentLimit( + bridges[_bridge].burnerParams.currentLimit, + bridges[_bridge].burnerParams.maxLimit, + bridges[_bridge].burnerParams.timestamp, + bridges[_bridge].burnerParams.ratePerSecond + ); + } + + /** + * @notice Uses the limit of any bridge + * @param _bridge The address of the bridge who is being changed + * @param _change The change in the limit + */ + function _useMinterLimits(address _bridge, uint256 _change) internal { + uint256 _currentLimit = mintingCurrentLimitOf(_bridge); + bridges[_bridge].minterParams.timestamp = block.timestamp; + bridges[_bridge].minterParams.currentLimit = _currentLimit - _change; + } + + /** + * @notice Uses the limit of any bridge + * @param _bridge The address of the bridge who is being changed + * @param _change The change in the limit + */ + function _useBurnerLimits(address _bridge, uint256 _change) internal { + uint256 _currentLimit = burningCurrentLimitOf(_bridge); + bridges[_bridge].burnerParams.timestamp = block.timestamp; + bridges[_bridge].burnerParams.currentLimit = _currentLimit - _change; + } + + /** + * @notice Updates the limit of any bridge + * @dev Can only be called by the owner + * @param _bridge The address of the bridge we are setting the limit too + * @param _limit The updated limit we are setting to the bridge + */ + function _changeMinterLimit(address _bridge, uint256 _limit) internal { + uint256 _oldLimit = bridges[_bridge].minterParams.maxLimit; + uint256 _currentLimit = mintingCurrentLimitOf(_bridge); + bridges[_bridge].minterParams.maxLimit = _limit; + + bridges[_bridge].minterParams.currentLimit = _calculateNewCurrentLimit(_limit, _oldLimit, _currentLimit); + + bridges[_bridge].minterParams.ratePerSecond = _limit / _DURATION; + bridges[_bridge].minterParams.timestamp = block.timestamp; + } + + /** + * @notice Updates the limit of any bridge + * @dev Can only be called by the owner + * @param _bridge The address of the bridge we are setting the limit too + * @param _limit The updated limit we are setting to the bridge + */ + function _changeBurnerLimit(address _bridge, uint256 _limit) internal { + uint256 _oldLimit = bridges[_bridge].burnerParams.maxLimit; + uint256 _currentLimit = burningCurrentLimitOf(_bridge); + bridges[_bridge].burnerParams.maxLimit = _limit; + + bridges[_bridge].burnerParams.currentLimit = _calculateNewCurrentLimit(_limit, _oldLimit, _currentLimit); + + bridges[_bridge].burnerParams.ratePerSecond = _limit / _DURATION; + bridges[_bridge].burnerParams.timestamp = block.timestamp; + } + + /** + * @notice Updates the current limit + * + * @param _limit The new limit + * @param _oldLimit The old limit + * @param _currentLimit The current limit + * @return _newCurrentLimit The new current limit + */ + function _calculateNewCurrentLimit( + uint256 _limit, + uint256 _oldLimit, + uint256 _currentLimit + ) internal pure returns (uint256 _newCurrentLimit) { + uint256 _difference; + + if (_oldLimit > _limit) { + _difference = _oldLimit - _limit; + _newCurrentLimit = _currentLimit > _difference ? _currentLimit - _difference : 0; + } else { + _difference = _limit - _oldLimit; + _newCurrentLimit = _currentLimit + _difference; + } + } + + /** + * @notice Gets the current limit + * + * @param _currentLimit The current limit + * @param _maxLimit The max limit + * @param _timestamp The timestamp of the last update + * @param _ratePerSecond The rate per second + * @return _limit The current limit + */ + function _getCurrentLimit( + uint256 _currentLimit, + uint256 _maxLimit, + uint256 _timestamp, + uint256 _ratePerSecond + ) internal view returns (uint256 _limit) { + _limit = _currentLimit; + if (_limit == _maxLimit) { + return _limit; + } else if (_timestamp + _DURATION <= block.timestamp) { + _limit = _maxLimit; + } else if (_timestamp + _DURATION > block.timestamp) { + uint256 _timePassed = block.timestamp - _timestamp; + uint256 _calculatedLimit = _limit + (_timePassed * _ratePerSecond); + _limit = _calculatedLimit > _maxLimit ? _maxLimit : _calculatedLimit; + } + } + + /** + * @notice Internal function for burning tokens + * + * @param _caller The caller address + * @param _user The user address + * @param _amount The amount to burn + */ + function _burnWithCaller(address _caller, address _user, uint256 _amount) internal { + if (_caller != lockbox) { + uint256 _currentLimit = burningCurrentLimitOf(_caller); + if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits(); + _useBurnerLimits(_caller, _amount); + } + _burn(_user, _amount); + } + + /** + * @notice Internal function for minting tokens + * + * @param _caller The caller address + * @param _user The user address + * @param _amount The amount to mint + */ + function _mintWithCaller(address _caller, address _user, uint256 _amount) internal { + if (_caller != lockbox) { + uint256 _currentLimit = mintingCurrentLimitOf(_caller); + if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits(); + _useMinterLimits(_caller, _amount); + } + _mint(_user, _amount); + } +} diff --git a/contracts/XERC20/XERC20Factory.sol b/contracts/XERC20/XERC20Factory.sol new file mode 100644 index 00000000..60bb1323 --- /dev/null +++ b/contracts/XERC20/XERC20Factory.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +import { XERC20 } from "./XERC20.sol"; +import { IXERC20Factory } from "./interfaces/IXERC20Factory.sol"; +import { XERC20Lockbox } from "./XERC20Lockbox.sol"; +import { CREATE3 } from "solmate/src/utils/CREATE3.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/** + * @custom:attribution https://github.com/defi-wonderland/xERC20/blob/dev/solidity/contracts/XERC20Factory.sol + */ +contract XERC20Factory is IXERC20Factory { + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @notice Address of the xerc20 maps to the address of its lockbox if it has one + */ + mapping(address => address) internal _lockboxRegistry; + + /** + * @notice The set of registered lockboxes + */ + EnumerableSet.AddressSet internal _lockboxRegistryArray; + + /** + * @notice The set of registered XERC20 tokens + */ + EnumerableSet.AddressSet internal _xerc20RegistryArray; + + /** + * @notice Deploys an XERC20 contract using CREATE3 + * @dev _limits and _minters must be the same length + * @param _name The name of the token + * @param _symbol The symbol of the token + * @param _minterLimits The array of limits that you are adding (optional, can be an empty array) + * @param _burnerLimits The array of limits that you are adding (optional, can be an empty array) + * @param _bridges The array of bridges that you are adding (optional, can be an empty array) + * @return _xerc20 The address of the xerc20 + */ + + function deployXERC20( + string memory _name, + string memory _symbol, + uint256[] memory _minterLimits, + uint256[] memory _burnerLimits, + address[] memory _bridges + ) external returns (address _xerc20) { + _xerc20 = _deployXERC20(_name, _symbol, _minterLimits, _burnerLimits, _bridges); + + emit XERC20Deployed(_xerc20); + } + + /** + * @notice Deploys an XERC20Lockbox contract using CREATE3 + * + * @dev When deploying a lockbox for the gas token of the chain, then, the base token needs to be address(0) + * @param _xerc20 The address of the xerc20 that you want to deploy a lockbox for + * @param _baseToken The address of the base token that you want to lock + * @param _isNative Whether or not the base token is the native (gas) token of the chain. Eg: MATIC for polygon chain + * @return _lockbox The address of the lockbox + */ + + function deployLockbox( + address _xerc20, + address _baseToken, + bool _isNative + ) external returns (address payable _lockbox) { + if ((_baseToken == address(0) && !_isNative) || (_isNative && _baseToken != address(0))) { + revert IXERC20Factory_BadTokenAddress(); + } + + if (XERC20(_xerc20).owner() != msg.sender) revert IXERC20Factory_NotOwner(); + if (_lockboxRegistry[_xerc20] != address(0)) revert IXERC20Factory_LockboxAlreadyDeployed(); + + _lockbox = _deployLockbox(_xerc20, _baseToken, _isNative); + + emit LockboxDeployed(_lockbox); + } + + /** + * @notice Deploys an XERC20 contract using CREATE3 + * @dev _limits and _minters must be the same length + * @param _name The name of the token + * @param _symbol The symbol of the token + * @param _minterLimits The array of limits that you are adding (optional, can be an empty array) + * @param _burnerLimits The array of limits that you are adding (optional, can be an empty array) + * @param _bridges The array of burners that you are adding (optional, can be an empty array) + * @return _xerc20 The address of the xerc20 + */ + + function _deployXERC20( + string memory _name, + string memory _symbol, + uint256[] memory _minterLimits, + uint256[] memory _burnerLimits, + address[] memory _bridges + ) internal returns (address _xerc20) { + uint256 _bridgesLength = _bridges.length; + if (_minterLimits.length != _bridgesLength || _burnerLimits.length != _bridgesLength) { + revert IXERC20Factory_InvalidLength(); + } + bytes32 _salt = keccak256(abi.encodePacked(_name, _symbol, msg.sender)); + bytes memory _creation = type(XERC20).creationCode; + bytes memory _bytecode = abi.encodePacked(_creation, abi.encode(_name, _symbol, address(this))); + + _xerc20 = CREATE3.deploy(_salt, _bytecode, 0); + + EnumerableSet.add(_xerc20RegistryArray, _xerc20); + + for (uint256 _i; _i < _bridgesLength; ++_i) { + XERC20(_xerc20).setLimits(_bridges[_i], _minterLimits[_i], _burnerLimits[_i]); + } + + XERC20(_xerc20).transferOwnership(msg.sender); + } + + /** + * @notice Deploys an XERC20Lockbox contract using CREATE3 + * + * @dev When deploying a lockbox for the gas token of the chain, then, the base token needs to be address(0) + * @param _xerc20 The address of the xerc20 that you want to deploy a lockbox for + * @param _baseToken The address of the base token that you want to lock + * @param _isNative Whether or not the base token is the native (gas) token of the chain. Eg: MATIC for polygon chain + * @return _lockbox The address of the lockbox + */ + function _deployLockbox( + address _xerc20, + address _baseToken, + bool _isNative + ) internal returns (address payable _lockbox) { + bytes32 _salt = keccak256(abi.encodePacked(_xerc20, _baseToken, msg.sender)); + bytes memory _creation = type(XERC20Lockbox).creationCode; + bytes memory _bytecode = abi.encodePacked(_creation, abi.encode(_xerc20, _baseToken, _isNative)); + + _lockbox = payable(CREATE3.deploy(_salt, _bytecode, 0)); + + XERC20(_xerc20).setLockbox(address(_lockbox)); + EnumerableSet.add(_lockboxRegistryArray, _lockbox); + _lockboxRegistry[_xerc20] = _lockbox; + } +} diff --git a/contracts/XERC20/XERC20Lockbox.sol b/contracts/XERC20/XERC20Lockbox.sol new file mode 100644 index 00000000..b2053883 --- /dev/null +++ b/contracts/XERC20/XERC20Lockbox.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IXERC20 } from "./interfaces/IXERC20.sol"; +import { IXERC20Lockbox } from "./interfaces/IXERC20Lockbox.sol"; + +/** + * @custom:attribution https://github.com/defi-wonderland/xERC20/blob/dev/solidity/contracts/XERC20Lockbox.sol + */ +contract XERC20Lockbox is IXERC20Lockbox { + using SafeERC20 for IERC20; + using SafeCast for uint256; + + /** + * @notice The XERC20 token of this contract + */ + IXERC20 public immutable XERC20; + + /** + * @notice The ERC20 token of this contract + */ + IERC20 public immutable ERC20; + + /** + * @notice Whether the ERC20 token is the native gas token of this chain + */ + + bool public immutable IS_NATIVE; + + /** + * @notice Constructor + * + * @param _xerc20 The address of the XERC20 contract + * @param _erc20 The address of the ERC20 contract + * @param _isNative Whether the ERC20 token is the native gas token of this chain or not + */ + + constructor(address _xerc20, address _erc20, bool _isNative) { + XERC20 = IXERC20(_xerc20); + ERC20 = IERC20(_erc20); + IS_NATIVE = _isNative; + } + + /** + * @notice Deposit native tokens into the lockbox + */ + + function depositNative() public payable { + if (!IS_NATIVE) revert IXERC20Lockbox_NotNative(); + + _deposit(msg.sender, msg.value); + } + + /** + * @notice Deposit ERC20 tokens into the lockbox + * + * @param _amount The amount of tokens to deposit + */ + + function deposit(uint256 _amount) external { + if (IS_NATIVE) revert IXERC20Lockbox_Native(); + + _deposit(msg.sender, _amount); + } + + /** + * @notice Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user + * + * @param _to The user to send the XERC20 to + * @param _amount The amount of tokens to deposit + */ + + function depositTo(address _to, uint256 _amount) external { + if (IS_NATIVE) revert IXERC20Lockbox_Native(); + + _deposit(_to, _amount); + } + + /** + * @notice Deposit the native asset into the lockbox, and send the XERC20 to a user + * + * @param _to The user to send the XERC20 to + */ + + function depositNativeTo(address _to) public payable { + if (!IS_NATIVE) revert IXERC20Lockbox_NotNative(); + + _deposit(_to, msg.value); + } + + /** + * @notice Withdraw ERC20 tokens from the lockbox + * + * @param _amount The amount of tokens to withdraw + */ + + function withdraw(uint256 _amount) external { + _withdraw(msg.sender, _amount); + } + + /** + * @notice Withdraw tokens from the lockbox + * + * @param _to The user to withdraw to + * @param _amount The amount of tokens to withdraw + */ + + function withdrawTo(address _to, uint256 _amount) external { + _withdraw(_to, _amount); + } + + /** + * @notice Withdraw tokens from the lockbox + * + * @param _to The user to withdraw to + * @param _amount The amount of tokens to withdraw + */ + + function _withdraw(address _to, uint256 _amount) internal { + emit Withdraw(_to, _amount); + + XERC20.burn(msg.sender, _amount); + + if (IS_NATIVE) { + (bool _success,) = payable(_to).call{value: _amount}(''); + if (!_success) revert IXERC20Lockbox_WithdrawFailed(); + } else { + ERC20.safeTransfer(_to, _amount); + } + } + + /** + * @notice Deposit tokens into the lockbox + * + * @param _to The address to send the XERC20 to + * @param _amount The amount of tokens to deposit + */ + + function _deposit(address _to, uint256 _amount) internal { + if (!IS_NATIVE) { + emit ABC(address(ERC20)); + ERC20.safeTransferFrom(msg.sender, address(this), _amount); + } + + XERC20.mint(_to, _amount); + emit Deposit(_to, _amount); + } + + event ABC(address abc); + + /** + * @notice Fallback function to deposit native tokens + */ + receive() external payable { + depositNative(); + } +} diff --git a/contracts/XERC20/interfaces/IXERC20.sol b/contracts/XERC20/interfaces/IXERC20.sol new file mode 100644 index 00000000..621eb2c1 --- /dev/null +++ b/contracts/XERC20/interfaces/IXERC20.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +/** + * @custom:attribution https://github.com/defi-wonderland/xERC20/blob/dev/solidity/interfaces/IXERC20.sol + */ +interface IXERC20 { + /** + * @notice Emits when a lockbox is set + * + * @param _lockbox The address of the lockbox + */ + event LockboxSet(address _lockbox); + + /** + * @notice Emits when a limit is set + * + * @param _mintingLimit The updated minting limit we are setting to the bridge + * @param _burningLimit The updated burning limit we are setting to the bridge + * @param _bridge The address of the bridge we are setting the limit too + */ + event BridgeLimitsSet(uint256 _mintingLimit, uint256 _burningLimit, address indexed _bridge); + + /** + * @notice Reverts when a user with too low of a limit tries to call mint/burn + */ + error IXERC20_NotHighEnoughLimits(); + + /** + * @notice Reverts when caller is not the factory + */ + error IXERC20_NotFactory(); + + /** + * @notice Reverts when limits are too high + */ + error IXERC20_LimitsTooHigh(); + + /** + * @notice Contains the full minting and burning data for a particular bridge + * + * @param minterParams The minting parameters for the bridge + * @param burnerParams The burning parameters for the bridge + */ + struct Bridge { + BridgeParameters minterParams; + BridgeParameters burnerParams; + } + + /** + * @notice Contains the mint or burn parameters for a bridge + * + * @param timestamp The timestamp of the last mint/burn + * @param ratePerSecond The rate per second of the bridge + * @param maxLimit The max limit of the bridge + * @param currentLimit The current limit of the bridge + */ + struct BridgeParameters { + uint256 timestamp; + uint256 ratePerSecond; + uint256 maxLimit; + uint256 currentLimit; + } + + /** + * @notice Sets the lockbox address + * + * @param _lockbox The address of the lockbox + */ + function setLockbox(address _lockbox) external; + + /** + * @notice Updates the limits of any bridge + * @dev Can only be called by the owner + * @param _mintingLimit The updated minting limit we are setting to the bridge + * @param _burningLimit The updated burning limit we are setting to the bridge + * @param _bridge The address of the bridge we are setting the limits too + */ + function setLimits(address _bridge, uint256 _mintingLimit, uint256 _burningLimit) external; + + /** + * @notice Returns the max limit of a minter + * + * @param _minter The minter we are viewing the limits of + * @return _limit The limit the minter has + */ + function mintingMaxLimitOf(address _minter) external view returns (uint256 _limit); + + /** + * @notice Returns the max limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + function burningMaxLimitOf(address _bridge) external view returns (uint256 _limit); + + /** + * @notice Returns the current limit of a minter + * + * @param _minter The minter we are viewing the limits of + * @return _limit The limit the minter has + */ + function mintingCurrentLimitOf(address _minter) external view returns (uint256 _limit); + + /** + * @notice Returns the current limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + function burningCurrentLimitOf(address _bridge) external view returns (uint256 _limit); + + /** + * @notice Mints tokens for a user + * @dev Can only be called by a minter + * @param _user The address of the user who needs tokens minted + * @param _amount The amount of tokens being minted + */ + function mint(address _user, uint256 _amount) external; + + /** + * @notice Burns tokens for a user + * @dev Can only be called by a minter + * @param _user The address of the user who needs tokens burned + * @param _amount The amount of tokens being burned + */ + function burn(address _user, uint256 _amount) external; +} diff --git a/contracts/XERC20/interfaces/IXERC20Factory.sol b/contracts/XERC20/interfaces/IXERC20Factory.sol new file mode 100644 index 00000000..99f41588 --- /dev/null +++ b/contracts/XERC20/interfaces/IXERC20Factory.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +interface IXERC20Factory { + /** + * @notice Emitted when a new XERC20 is deployed + * + * @param _xerc20 The address of the xerc20 + */ + + event XERC20Deployed(address _xerc20); + + /** + * @notice Emitted when a new XERC20Lockbox is deployed + * + * @param _lockbox The address of the lockbox + */ + + event LockboxDeployed(address _lockbox); + + /** + * @notice Reverts when a non-owner attempts to call + */ + + error IXERC20Factory_NotOwner(); + + /** + * @notice Reverts when a lockbox is trying to be deployed from a malicious address + */ + + error IXERC20Factory_BadTokenAddress(); + + /** + * @notice Reverts when a lockbox is already deployed + */ + + error IXERC20Factory_LockboxAlreadyDeployed(); + + /** + * @notice Reverts when a the length of arrays sent is incorrect + */ + error IXERC20Factory_InvalidLength(); + + /** + * @notice Deploys an XERC20 contract using CREATE3 + * @dev _limits and _minters must be the same length + * @param _name The name of the token + * @param _symbol The symbol of the token + * @param _minterLimits The array of minter limits that you are adding (optional, can be an empty array) + * @param _burnerLimits The array of burning limits that you are adding (optional, can be an empty array) + * @param _bridges The array of burners that you are adding (optional, can be an empty array) + * @return _xerc20 The address of the xerc20 + */ + + function deployXERC20( + string memory _name, + string memory _symbol, + uint256[] memory _minterLimits, + uint256[] memory _burnerLimits, + address[] memory _bridges + ) external returns (address _xerc20); + + /** + * @notice Deploys an XERC20Lockbox contract using CREATE3 + * + * @param _xerc20 The address of the xerc20 that you want to deploy a lockbox for + * @param _baseToken The address of the base token that you want to lock + * @param _isNative Whether or not the base token is native + * @return _lockbox The address of the lockbox + */ + + function deployLockbox( + address _xerc20, + address _baseToken, + bool _isNative + ) external returns (address payable _lockbox); +} diff --git a/contracts/XERC20/interfaces/IXERC20Lockbox.sol b/contracts/XERC20/interfaces/IXERC20Lockbox.sol new file mode 100644 index 00000000..dd98ac04 --- /dev/null +++ b/contracts/XERC20/interfaces/IXERC20Lockbox.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +/** + * @custom:attribution https://github.com/defi-wonderland/xERC20/blob/dev/solidity/interfaces/IXERC20Lockbox.sol + */ +interface IXERC20Lockbox { + /** + * @notice Emitted when tokens are deposited into the lockbox + * + * @param _sender The address of the user who deposited + * @param _amount The amount of tokens deposited + */ + + event Deposit(address _sender, uint256 _amount); + + /** + * @notice Emitted when tokens are withdrawn from the lockbox + * + * @param _sender The address of the user who withdrew + * @param _amount The amount of tokens withdrawn + */ + + event Withdraw(address _sender, uint256 _amount); + + /** + * @notice Reverts when a user tries to deposit native tokens on a non-native lockbox + */ + + error IXERC20Lockbox_NotNative(); + + /** + * @notice Reverts when a user tries to deposit non-native tokens on a native lockbox + */ + + error IXERC20Lockbox_Native(); + + /** + * @notice Reverts when a user tries to withdraw and the call fails + */ + + error IXERC20Lockbox_WithdrawFailed(); + + /** + * @notice Deposit ERC20 tokens into the lockbox + * + * @param _amount The amount of tokens to deposit + */ + + function deposit(uint256 _amount) external; + + /** + * @notice Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user + * + * @param _user The user to send the XERC20 to + * @param _amount The amount of tokens to deposit + */ + + function depositTo(address _user, uint256 _amount) external; + + /** + * @notice Deposit the native asset into the lockbox, and send the XERC20 to a user + * + * @param _user The user to send the XERC20 to + */ + + function depositNativeTo(address _user) external payable; + + /** + * @notice Withdraw ERC20 tokens from the lockbox + * + * @param _amount The amount of tokens to withdraw + */ + + function withdraw(uint256 _amount) external; + + /** + * @notice Withdraw ERC20 tokens from the lockbox + * + * @param _user The user to withdraw to + * @param _amount The amount of tokens to withdraw + */ + + function withdrawTo(address _user, uint256 _amount) external; +} diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol new file mode 100644 index 00000000..f9b40c50 --- /dev/null +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -0,0 +1,114 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import "../../contracts/interfaces/IBridge.sol"; +import "../../contracts/interfaces/IFeeHandler.sol"; +import "../XERC20/interfaces/IXERC20.sol"; + +contract GmpTransferAdapter is AccessControl { + using ERC165Checker for address; + + IBridge public immutable _bridge; + bytes32 public immutable _resourceID; + address immutable _gmpAddress; + + + event Withdrawal(address recipient, uint amount); + event FundsTransferred(address recipient, uint256 amount); + + error SenderNotAdmin(); + error InsufficientMsgValueAmount(uint256 amount); + error MsgValueLowerThanFee(uint256 amount); + error TokenWithdrawalFailed(); + error InsufficientBalance(); + error InvalidHandler(address handler); + error InvalidOriginAdapter(address adapter); + error FailedFundsTransfer(); + + error Invalid_MsgNotZero(); + error FailedRefund(); + + modifier onlyAdmin() { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert SenderNotAdmin(); + _; + } + + /** + @notice This contract requires for transfer that the origin adapter address is the same across all networks. + Because of that it should be deployed using multichain deployer or create2. + */ + constructor(IBridge bridge, address newGmpAddress, bytes32 resourceID) { + _bridge = bridge; + _gmpAddress = newGmpAddress; + _resourceID = resourceID; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function deposit(uint8 destinationDomainID, address recipientAddress, address XERC20Address, uint256 tokenAmount) external payable { + address feeHandlerRouter = _bridge._feeHandler(); + (uint256 fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + _resourceID, + "", // depositData - not parsed + "" // feeData - not parsed + ); + + if (msg.value < fee) { + revert InsufficientMsgValueAmount(msg.value); + // refund excess msg.value + } else if (msg.value > fee) { + (bool success, ) = msg.sender.call{value: msg.value - fee}(""); + if (!success) revert FailedRefund(); + } + + bytes memory depositData = abi.encodePacked( + // uint256 maxFee + uint256(950000), + // uint16 len(executeFuncSignature) + uint16(4), + // bytes executeFuncSignature + IXERC20(address(0)).mint.selector, + // uint8 len(executeContractAddress) + uint8(20), + // bytes executeContractAddress + XERC20Address, + // uint8 len(executionDataDepositor) + uint8(20), + // bytes executionDataDepositor + address(this), + // bytes executionDataDepositor + executionData + prepareDepositData(recipientAddress, XERC20Address, tokenAmount) + ); + + IXERC20(XERC20Address).burn(msg.sender, tokenAmount); + + _bridge.deposit{value: fee}(destinationDomainID, _resourceID, depositData, ""); + } + + function executeProposal(address gmpAdapter, address recipient, address XERC20Address, uint256 amount) external { + if (gmpAdapter != address(this)) revert InvalidOriginAdapter(gmpAdapter); + if (msg.sender != _gmpAddress) revert InvalidHandler(msg.sender); + + IXERC20(XERC20Address).mint(recipient, amount); + } + + function slice(bytes calldata input, uint256 position) public pure returns (bytes memory) { + return input[position:]; + } + + function prepareDepositData( + address recipientAddress, + address XERC20Address, + uint256 bridgingAmount + ) public view returns (bytes memory) { + bytes memory encoded = abi.encode(address(0), recipientAddress, XERC20Address, bridgingAmount); + return this.slice(encoded, 32); + } + + receive() external payable {} +} diff --git a/contracts/interfaces/IBridge.sol b/contracts/interfaces/IBridge.sol index 0292a95e..29b551fd 100644 --- a/contracts/interfaces/IBridge.sol +++ b/contracts/interfaces/IBridge.sol @@ -13,10 +13,32 @@ interface IBridge { */ function _domainID() external returns (uint8); + /** + @notice Exposing getter for {_feeHandler} instead of forcing the use of call. + @return address The {_feeHandler} that is currently set for the Bridge contract. + */ + function _feeHandler() external returns (address); + /** @notice Exposing getter for {_resourceIDToHandlerAddress}. @param resourceID ResourceID to be used when making deposits. @return address The {handlerAddress} that is currently set for the resourceID. */ function _resourceIDToHandlerAddress(bytes32 resourceID) external view returns (address); -} \ No newline at end of file + + /** + @notice Initiates a transfer using a specified handler contract. + @notice Only callable when Bridge is not paused. + @param destinationDomainID ID of chain deposit will be bridged to. + @param resourceID ResourceID used to find address of handler to be used for deposit. + @param depositData Additional data to be passed to specified handler. + @param feeData Additional data to be passed to the fee handler. + @notice Emits {Deposit} event with all necessary parameters. + */ + function deposit( + uint8 destinationDomainID, + bytes32 resourceID, + bytes calldata depositData, + bytes calldata feeData + ) external payable returns (uint64 depositNonce, bytes memory handlerResponse); +} diff --git a/package.json b/package.json index 25f82470..f4d6bf67 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ }, "dependencies": { "@openzeppelin/contracts": "4.5.0", - "@uniswap/v3-periphery": "^1.4.4" + "@uniswap/v3-periphery": "^1.4.4", + "solmate": "^6.2.0" } } diff --git a/test/gmpTransferAdapter/collectFee.js b/test/gmpTransferAdapter/collectFee.js new file mode 100644 index 00000000..c05847a9 --- /dev/null +++ b/test/gmpTransferAdapter/collectFee.js @@ -0,0 +1,186 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); +const Helpers = require("../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); + + +contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[3]; + + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const excessFee = Ethers.utils.parseEther("1"); + const transferredAmount = 10 + const mintingLimit = 500; + const burningLimit = 500; + + + + let BridgeInstance; + let GmpTransferAdapterInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let depositFunctionSignature; + let GmpHandlerInstance; + let XERC20LockboxInstance; + let XERC20Instance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + accounts[0] + )) + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + XERC20FactoryInstance = await XERC20FactoryContract.new(); + const XERC20DeployResponse = await XERC20FactoryInstance.deployXERC20( + "sygmaETH", + "sETH", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set XERC20 contract instance address to the address deployed via XERC20Factory + const deployedXERC20Address = XERC20DeployResponse.logs[0].args._xerc20 + XERC20Instance = await XERC20Contract.at(deployedXERC20Address) + const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( + XERC20Instance.address, + Ethers.constants.AddressZero, + true + ); + // set Lockbox contract instance address to the address deployed via XERC20Factory + const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox + XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); + + await XERC20LockboxInstance.depositNativeTo(depositorAddress, {value: transferredAmount}); + await XERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + await BasicFeeHandlerInstance.changeFee(originDomainID, resourceID, fee); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("should successfully charge fee on deposit", async () => { + const feeHandlerBalanceBefore = await web3.eth.getBalance(BasicFeeHandlerInstance.address); + + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee, + } + ) + ); + const feeHandlerBalanceAfter = await web3.eth.getBalance(BasicFeeHandlerInstance.address); + assert.strictEqual( + Ethers.BigNumber.from(feeHandlerBalanceBefore).add(fee.toString()).toString(), + feeHandlerBalanceAfter.toString() + ); + }); + + it("should refund the depositor if too much ETH is sent as fee", async () => { + const feeHandlerBalanceBefore = await web3.eth.getBalance(BasicFeeHandlerInstance.address); + const depositorNativeBalanceBefore = await web3.eth.getBalance(depositorAddress); + + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: excessFee, + } + ) + ); + const feeHandlerBalanceAfter = await web3.eth.getBalance(BasicFeeHandlerInstance.address); + const depositorNativeBalanceAfter = await web3.eth.getBalance(depositorAddress); + assert.strictEqual( + Ethers.BigNumber.from(feeHandlerBalanceBefore).add(fee.toString()).toString(), + feeHandlerBalanceAfter.toString() + ); + expect( + Number(Ethers.utils.formatEther(new Ethers.BigNumber.from(depositorNativeBalanceBefore).sub(fee))) + ).to.be.within( + Number(Ethers.utils.formatEther(depositorNativeBalanceAfter))*0.99, + Number(Ethers.utils.formatEther(depositorNativeBalanceAfter))*1.01 + ); + }); +}); diff --git a/test/gmpTransferAdapter/erc20Transfer/deposit.js b/test/gmpTransferAdapter/erc20Transfer/deposit.js new file mode 100644 index 00000000..cb6769ce --- /dev/null +++ b/test/gmpTransferAdapter/erc20Transfer/deposit.js @@ -0,0 +1,201 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); +const Helpers = require("../../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + + +contract("Gmp transfer adapter - [Deposit - ERC20 token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const expectedDepositNonce = 1; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[3]; + + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const mintingLimit = 500; + const burningLimit = 500; + + + + let BridgeInstance; + let GmpTransferAdapterInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let depositFunctionSignature; + let GmpHandlerInstance; + let XERC20LockboxInstance; + let XERC20Instance; + let ERC20MintableInstance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + accounts[0] + )), + ERC20MintableContract.new("token", "TOK").then( + (instance) => (ERC20MintableInstance = instance) + ), + ]); + + await ERC20MintableInstance.mint(depositorAddress, depositAmount); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + XERC20FactoryInstance = await XERC20FactoryContract.new(); + const XERC20DeployResponse = await XERC20FactoryInstance.deployXERC20( + "sygmaETH", + "sETH", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set XERC20 contract instance address to the address deployed via XERC20Factory + const deployedXERC20Address = XERC20DeployResponse.logs[0].args._xerc20 + XERC20Instance = await XERC20Contract.at(deployedXERC20Address) + const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( + XERC20Instance.address, + ERC20MintableInstance.address, + false + ); + // set Lockbox contract instance address to the address deployed via XERC20Factory + const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox + XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); + + await ERC20MintableInstance.increaseAllowance( + XERC20LockboxInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + await XERC20LockboxInstance.depositTo( + depositorAddress, + depositAmount, + { + from: depositorAddress + } + ); + await XERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + await BasicFeeHandlerInstance.changeFee(originDomainID, resourceID, fee); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("deposit can be made successfully and depositor tokens are burnt", async () => { + const depositorXERC20BalanceBefore = await XERC20Instance.balanceOf(depositorAddress); + + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee, + } + ) + ); + const depositorXERC20BalanceAfter = await XERC20Instance.balanceOf(depositorAddress); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).sub(depositorXERC20BalanceBefore.toString()).toString(), + depositorXERC20BalanceAfter.toString() + ); + }); + + it("depositEvent is emitted with expected values", async () => { + const depositTx = await GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee, + } + ); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === originDomainID && + event.resourceID === resourceID.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === GmpTransferAdapterInstance.address + ); + }); + }); +}); diff --git a/test/gmpTransferAdapter/erc20Transfer/executeProposal.js b/test/gmpTransferAdapter/erc20Transfer/executeProposal.js new file mode 100644 index 00000000..20b8ec31 --- /dev/null +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposal.js @@ -0,0 +1,372 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); + +const Helpers = require("../../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract("Gmp transfer adapter - [Execute proposal - ERC20 token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const recipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const handlerResponseLength = 64; + const contractCallReturndata = Ethers.constants.HashZero; + const destinationMaxFee = 900000; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = 10; + const mintingLimit = 500; + const burningLimit = 500; + + let BridgeInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let XERC20Instance; + let XERC20LockboxInstance; + let proposal; + let dataHash; + let depositFunctionSignature; + let ERC20MintableInstance; + + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + adminAddress + )), + ERC20MintableContract.new("token", "TOK").then( + (instance) => (ERC20MintableInstance = instance) + ), + ]); + + await ERC20MintableInstance.mint(depositorAddress, depositAmount); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + XERC20FactoryInstance = await XERC20FactoryContract.new(); + const XERC20DeployResponse = await XERC20FactoryInstance.deployXERC20( + "sygmaToken", + "sTOK", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set XERC20 contract instance address to the address deployed via XERC20Factory + const deployedXERC20Address = XERC20DeployResponse.logs[0].args._xerc20 + XERC20Instance = await XERC20Contract.at(deployedXERC20Address) + const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( + XERC20Instance.address, + ERC20MintableInstance.address, + false + ); + // set Lockbox contract instance address to the address deployed via XERC20Factory + const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox + XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); + + await ERC20MintableInstance.increaseAllowance( + XERC20LockboxInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + await XERC20LockboxInstance.depositTo( + depositorAddress, + depositAmount, + { + from: depositorAddress + } + ); + await XERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + const preparedExecutionData = await GmpTransferAdapterInstance.prepareDepositData( + recipientAddress, + XERC20Instance.address, + transferredAmount + ); + depositData = Helpers.createGmpDepositData( + depositFunctionSignature, + GmpTransferAdapterInstance.address, + destinationMaxFee, + GmpTransferAdapterInstance.address, + preparedExecutionData + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositData, + }; + + dataHash = Ethers.utils.keccak256( + GmpHandlerInstance.address + depositData.substr(2) + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("isProposalExecuted returns false if depositNonce is not used", async () => { + const destinationDomainID = await BridgeInstance._domainID(); + + assert.isFalse( + await BridgeInstance.isProposalExecuted( + destinationDomainID, + expectedDepositNonce + ) + ); + }); + + it("should create and execute executeProposal successfully", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + const depositorXERC20BalanceBefore = await XERC20Instance.balanceOf(depositorAddress); + const recipientXERC20BalanceBefore = await XERC20Instance.balanceOf(recipientAddress); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }); + + const recipientNativeBalanceBefore = await web3.eth.getBalance(recipientAddress); + const depositorXERC20BalanceAfter = await XERC20Instance.balanceOf(depositorAddress); + const recipientXERC20BalanceAfter = await XERC20Instance.balanceOf(recipientAddress); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that depositor and recipient balances are aligned with expectations + const recipientNativeBalanceAfter = await web3.eth.getBalance(recipientAddress); + assert.strictEqual(recipientNativeBalanceBefore, recipientNativeBalanceAfter); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).sub(depositorXERC20BalanceBefore.toString()).toString(), + depositorXERC20BalanceAfter.toString() + ); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).add(recipientXERC20BalanceBefore.toString()).toString(), + recipientXERC20BalanceAfter.toString() + ); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).add(recipientXERC20BalanceBefore.toString()).toString(), + recipientXERC20BalanceAfter.toString() + ); + }); + + it("should skip executing proposal if deposit nonce is already used", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }) + ); + + const skipExecuteTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + // check that no ProposalExecution events are emitted + assert.equal(skipExecuteTx.logs.length, 0); + }); + + it("executeProposal event should be emitted with expected values", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + const recipientBalanceBefore = await web3.eth.getBalance(recipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["bool", "uint256", "bytes32"], + [true, handlerResponseLength, contractCallReturndata] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + + // check that recipient native token balance hasn't changed + const recipientBalanceAfter = await web3.eth.getBalance(recipientAddress); + assert.strictEqual(recipientBalanceBefore, recipientBalanceAfter); + }); + + it(`should fail to executeProposal if signed Proposal has different + chainID than the one on which it should be executed`, async () => { + const proposalSignedData = + await Helpers.mockSignTypedProposalWithInvalidChainID( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await Helpers.expectToRevertWithCustomError( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }), + "InvalidProposalSigner()" + ); + }); +}); diff --git a/test/gmpTransferAdapter/nativeTransfer/deposit.js b/test/gmpTransferAdapter/nativeTransfer/deposit.js new file mode 100644 index 00000000..2bb60970 --- /dev/null +++ b/test/gmpTransferAdapter/nativeTransfer/deposit.js @@ -0,0 +1,185 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); +const Helpers = require("../../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); + + +contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const expectedDepositNonce = 1; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[3]; + + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = 10 + const mintingLimit = 500; + const burningLimit = 500; + + + + let BridgeInstance; + let GmpTransferAdapterInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let depositFunctionSignature; + let GmpHandlerInstance; + let XERC20LockboxInstance; + let XERC20Instance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + accounts[0] + )) + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + XERC20FactoryInstance = await XERC20FactoryContract.new(); + const XERC20DeployResponse = await XERC20FactoryInstance.deployXERC20( + "sygmaETH", + "sETH", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set XERC20 contract instance address to the address deployed via XERC20Factory + const deployedXERC20Address = XERC20DeployResponse.logs[0].args._xerc20 + XERC20Instance = await XERC20Contract.at(deployedXERC20Address) + const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( + XERC20Instance.address, + Ethers.constants.AddressZero, + true + ); + // set Lockbox contract instance address to the address deployed via XERC20Factory + const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox + XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); + + await XERC20LockboxInstance.depositNativeTo(depositorAddress, {value: transferredAmount}); + await XERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + await BasicFeeHandlerInstance.changeFee(originDomainID, resourceID, fee); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("deposit can be made successfully and depositor native tokens are deducted", async () => { + const depositorNativeBalanceBefore = await web3.eth.getBalance(depositorAddress); + + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee, + } + ) + ); + const depositorNativeBalanceAfter = await web3.eth.getBalance(depositorAddress); + expect( + Number(Ethers.utils.formatEther(new Ethers.BigNumber.from(depositorNativeBalanceBefore).add(fee))) + ).to.be.within( + Number(Ethers.utils.formatEther(depositorNativeBalanceAfter))*0.99, + Number(Ethers.utils.formatEther(depositorNativeBalanceAfter))*1.01 + ); + }); + + it("depositEvent is emitted with expected values", async () => { + const depositTx = await GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee, + } + ); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === originDomainID && + event.resourceID === resourceID.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === GmpTransferAdapterInstance.address + ); + }); + }); +}); diff --git a/test/gmpTransferAdapter/nativeTransfer/executeProposal.js b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js new file mode 100644 index 00000000..e9e8f253 --- /dev/null +++ b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js @@ -0,0 +1,359 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); + +const Helpers = require("../../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); + +contract("Gmp transfer adapter - [Execute proposal - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const recipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const handlerResponseLength = 64; + const contractCallReturndata = Ethers.constants.HashZero; + const destinationMaxFee = 900000; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = 10 + const mintingLimit = 500; + const burningLimit = 500; + + let BridgeInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let XERC20Instance; + let XERC20LockboxInstance; + let proposal; + let dataHash; + let depositFunctionSignature; + + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + XERC20FactoryInstance = await XERC20FactoryContract.new(); + const response = await XERC20FactoryInstance.deployXERC20( + "sygmaETH", + "sETH", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set XERC20 contract instance address to the address deployed via XERC20Factory + const deployedXERC20Address = response.logs[0].args._xerc20 + XERC20Instance = await XERC20Contract.at(deployedXERC20Address) + const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( + XERC20Instance.address, + Ethers.constants.AddressZero, + true + ); + // set Lockbox contract instance address to the address deployed via XERC20Factory + const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox + XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); + + await XERC20LockboxInstance.depositNativeTo( + depositorAddress, + { + value: depositAmount + } + ); + await XERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + const preparedExecutionData = await GmpTransferAdapterInstance.prepareDepositData( + recipientAddress, + XERC20Instance.address, + transferredAmount + ); + depositData = Helpers.createGmpDepositData( + depositFunctionSignature, + GmpTransferAdapterInstance.address, + destinationMaxFee, + GmpTransferAdapterInstance.address, + preparedExecutionData + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositData, + }; + + dataHash = Ethers.utils.keccak256( + GmpHandlerInstance.address + depositData.substr(2) + ); + + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("isProposalExecuted returns false if depositNonce is not used", async () => { + const destinationDomainID = await BridgeInstance._domainID(); + + assert.isFalse( + await BridgeInstance.isProposalExecuted( + destinationDomainID, + expectedDepositNonce + ) + ); + }); + + it("should create and execute executeProposal successfully", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + const recipientNativeBalanceBefore = await web3.eth.getBalance(recipientAddress); + const depositorXERC20BalanceBefore = await XERC20Instance.balanceOf(depositorAddress); + const recipientXERC20BalanceBefore = await XERC20Instance.balanceOf(recipientAddress); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }); + + const depositorXERC20BalanceAfter = await XERC20Instance.balanceOf(depositorAddress); + const recipientXERC20BalanceAfter = await XERC20Instance.balanceOf(recipientAddress); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that depositor and recipient balances are aligned with expectations + const recipientNativeBalanceAfter = await web3.eth.getBalance(recipientAddress); + assert.strictEqual(recipientNativeBalanceBefore, recipientNativeBalanceAfter); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).sub(depositorXERC20BalanceBefore.toString()).toString(), + depositorXERC20BalanceAfter.toString() + ); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).add(recipientXERC20BalanceBefore.toString()).toString(), + recipientXERC20BalanceAfter.toString() + ); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).add(recipientXERC20BalanceBefore.toString()).toString(), + recipientXERC20BalanceAfter.toString() + ); + }); + + it("should skip executing proposal if deposit nonce is already used", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }) + ); + + const skipExecuteTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + // check that no ProposalExecution events are emitted + assert.equal(skipExecuteTx.logs.length, 0); + }); + + it("executeProposal event should be emitted with expected values", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + const recipientBalanceBefore = await web3.eth.getBalance(recipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["bool", "uint256", "bytes32"], + [true, handlerResponseLength, contractCallReturndata] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + + // check that recipient native token balance hasn't changed + const recipientBalanceAfter = await web3.eth.getBalance(recipientAddress); + assert.strictEqual(recipientBalanceBefore, recipientBalanceAfter); + }); + + it(`should fail to executeProposal if signed Proposal has different + chainID than the one on which it should be executed`, async () => { + const proposalSignedData = + await Helpers.mockSignTypedProposalWithInvalidChainID( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + XERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await Helpers.expectToRevertWithCustomError( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }), + "InvalidProposalSigner()" + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 2b3e485a..ef7bdd82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10088,6 +10088,11 @@ solidity-coverage@^0.7.20: shelljs "^0.8.3" web3-utils "^1.3.0" +solmate@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/solmate/-/solmate-6.2.0.tgz#edd29b5f3d6faafafdcf65fe4d1d959b4841cfa8" + integrity sha512-AM38ioQ2P8zRsA42zenb9or6OybRjOLXIu3lhIT8rhddUuduCt76pUEuLxOIg9GByGojGz+EbpFdCB6B+QZVVA== + source-map-support@0.5.12: version "0.5.12" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" From 68e788ba566abec32fc7150078687bb029460abd Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Wed, 31 Jul 2024 16:03:02 +0200 Subject: [PATCH 02/14] run tests with Paris hardfork --- scripts/start_ganache.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/start_ganache.sh b/scripts/start_ganache.sh index bce7a9cc..4cfd75f4 100755 --- a/scripts/start_ganache.sh +++ b/scripts/start_ganache.sh @@ -10,9 +10,9 @@ PORT=${PORT:-8545} echo "Running ganache..." if [[ $SILENT ]]; then - ganache-cli -q -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" & + ganache-cli -k "merge" -q -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" & # Otherwise CI will run tests before ganache has started sleep 3 else - ganache-cli -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" -fi \ No newline at end of file + ganache-cli -k "merge" -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" +fi From 2ae07fbf35d6ded669dc3040a2e30a66f260e762 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Wed, 31 Jul 2024 16:21:13 +0200 Subject: [PATCH 03/14] remove unused events --- contracts/adapters/GmpTransferAdapter.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol index f9b40c50..00ea63d4 100644 --- a/contracts/adapters/GmpTransferAdapter.sol +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -21,12 +21,8 @@ contract GmpTransferAdapter is AccessControl { error SenderNotAdmin(); error InsufficientMsgValueAmount(uint256 amount); - error MsgValueLowerThanFee(uint256 amount); - error TokenWithdrawalFailed(); - error InsufficientBalance(); error InvalidHandler(address handler); error InvalidOriginAdapter(address adapter); - error FailedFundsTransfer(); error Invalid_MsgNotZero(); error FailedRefund(); From e94e3ac9cf57b918eb4a8c06d0cc3210016d2700 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Wed, 31 Jul 2024 16:38:57 +0200 Subject: [PATCH 04/14] remove excess code --- contracts/adapters/GmpTransferAdapter.sol | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol index 00ea63d4..1d32b81e 100644 --- a/contracts/adapters/GmpTransferAdapter.sol +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -2,36 +2,24 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.11; -import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import "../../contracts/interfaces/IBridge.sol"; import "../../contracts/interfaces/IFeeHandler.sol"; import "../XERC20/interfaces/IXERC20.sol"; -contract GmpTransferAdapter is AccessControl { +contract GmpTransferAdapter { using ERC165Checker for address; IBridge public immutable _bridge; bytes32 public immutable _resourceID; address immutable _gmpAddress; - - event Withdrawal(address recipient, uint amount); - event FundsTransferred(address recipient, uint256 amount); - - error SenderNotAdmin(); error InsufficientMsgValueAmount(uint256 amount); error InvalidHandler(address handler); error InvalidOriginAdapter(address adapter); - error Invalid_MsgNotZero(); error FailedRefund(); - modifier onlyAdmin() { - if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert SenderNotAdmin(); - _; - } - /** @notice This contract requires for transfer that the origin adapter address is the same across all networks. Because of that it should be deployed using multichain deployer or create2. @@ -40,7 +28,6 @@ contract GmpTransferAdapter is AccessControl { _bridge = bridge; _gmpAddress = newGmpAddress; _resourceID = resourceID; - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } function deposit(uint8 destinationDomainID, address recipientAddress, address XERC20Address, uint256 tokenAmount) external payable { From 632a27344535054befb7da8c908f8a841433a1cd Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Thu, 1 Aug 2024 23:24:40 +0700 Subject: [PATCH 05/14] Update ganache --- Dockerfile | 4 +- Makefile | 4 +- package.json | 2 +- scripts/install_deps.sh | 6 +- scripts/start_ganache.sh | 4 +- test/contractBridge/admin.js | 4 +- test/contractBridge/depositERC20.js | 2 +- test/contractBridge/depositXC20.js | 2 +- test/contractBridge/executeProposalERC20.js | 2 +- test/e2e/erc1155/differentChainsMock.js | 4 +- test/e2e/erc1155/sameChain.js | 6 +- test/e2e/erc721/differentChainsMock.js | 8 +- test/forwarder/forwarder.js | 19 +- test/frostKeygen/frostKeygen.js | 5 +- test/gasBenchmarks/deployments.js | 4 +- test/handlers/erc1155/depositBurn.js | 4 +- test/handlers/erc20/deposit.js | 4 +- test/handlers/erc20/depositBurn.js | 4 +- test/handlers/erc721/depositBurn.js | 8 +- test/handlers/fee/basic/admin.js | 2 +- test/handlers/fee/basic/changeFee.js | 4 +- test/handlers/fee/basic/collectFee.js | 14 +- test/handlers/fee/basic/distributeFee.js | 4 +- test/handlers/fee/handlerRouter.js | 6 +- test/handlers/fee/percentage/admin.js | 2 +- test/handlers/fee/percentage/changeFee.js | 8 +- test/handlers/fee/percentage/collectFee.js | 10 +- test/handlers/fee/percentage/distributeFee.js | 11 +- .../handlers/generic/permissionlessDeposit.js | 6 +- test/handlers/xc20/deposit.js | 4 +- test/handlers/xc20/depositBurn.js | 4 +- test/helpers.js | 35 ++- .../accessControlSegregator/constructor.js | 3 +- .../accessControlSegregator/grantAccess.js | 3 +- truffle-config.js | 2 +- yarn.lock | 240 ++++++++++-------- 36 files changed, 248 insertions(+), 206 deletions(-) diff --git a/Dockerfile b/Dockerfile index 846ab6e0..d705af8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM trufflesuite/ganache-cli +FROM trufflesuite/ganache WORKDIR /app COPY data/ /app/data -CMD ["ganache-cli", "--db", "data/", "-h", "0.0.0.0", "-p", "8545"] +CMD ["ganache", "--db", "data/", "-h", "0.0.0.0", "-p", "8545"] diff --git a/Makefile b/Makefile index 8b952083..8e0f4468 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,11 @@ install-deps: .PHONY: test test: @echo " > \033[32mTesting contracts... \033[0m " - npx truffle test + truffle test --stacktrace compile: @echo " > \033[32mCompiling contracts... \033[0m " - npx truffle compile + truffle compile start-ganache: @echo " > \033[32mStarting ganache... \033[0m " diff --git a/package.json b/package.json index f4d6bf67..ed8c3b5b 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "eth-sig-util": "^3.0.1", "ethereumjs-wallet": "^1.0.2", "ethers": "^5.5.4", - "ganache-cli": "^6.12.2", + "ganache": "^7.9.2", "lodash.template": "^4.5.0", "minimist": "^1.2.7", "prettier": "2.8.1", diff --git a/scripts/install_deps.sh b/scripts/install_deps.sh index 461fa1fa..4ae7a012 100755 --- a/scripts/install_deps.sh +++ b/scripts/install_deps.sh @@ -14,11 +14,11 @@ else (set -x; npm install --global truffle) fi -if [ -x "$(command -v ganache-cli)" ] +if [ -x "$(command -v ganache)" ] then - echo "ganache-cli found, skipping install" + echo "ganache found, skipping install" else - (set -x; npm install --global ganache-cli) + (set -x; npm install --global ganache) fi if [ -x "$(command -v abigen)" ] diff --git a/scripts/start_ganache.sh b/scripts/start_ganache.sh index 4cfd75f4..9fff5da1 100755 --- a/scripts/start_ganache.sh +++ b/scripts/start_ganache.sh @@ -10,9 +10,9 @@ PORT=${PORT:-8545} echo "Running ganache..." if [[ $SILENT ]]; then - ganache-cli -k "merge" -q -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" & + ganache -k london --chain.asyncRequestProcessing false --chain.chainId 1 -q -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" & # Otherwise CI will run tests before ganache has started sleep 3 else - ganache-cli -k "merge" -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" + ganache -k london --chain.asyncRequestProcessing false --chain.chainId 1 -p $PORT --account "0x000000000000000000000000000000000000000000000000000000616c696365,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000626f62,100000000000000000000" --account "0x00000000000000000000000000000000000000000000000000636861726c6965,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000064617665,100000000000000000000" --account "0x0000000000000000000000000000000000000000000000000000000000657665,100000000000000000000" fi diff --git a/test/contractBridge/admin.js b/test/contractBridge/admin.js index 2aab2c05..2fed8933 100644 --- a/test/contractBridge/admin.js +++ b/test/contractBridge/admin.js @@ -262,7 +262,7 @@ contract("Bridge - [admin]", async (accounts) => { domainID ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.adminSetResource( ERC1155HandlerInstance.address, invalidResourceID, @@ -427,7 +427,7 @@ contract("Bridge - [admin]", async (accounts) => { const currentNonce = 3; await BridgeInstance.adminSetDepositNonce(domainID, currentNonce); const newNonce = 2; - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.adminSetDepositNonce(domainID, newNonce), "Does not allow decrements of the nonce" ); diff --git a/test/contractBridge/depositERC20.js b/test/contractBridge/depositERC20.js index 3090c1fb..f66ed83b 100644 --- a/test/contractBridge/depositERC20.js +++ b/test/contractBridge/depositERC20.js @@ -225,7 +225,7 @@ contract("Bridge - [deposit - ERC20]", async (accounts) => { }); it("should revert if ERC20Safe contract call fails", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, initialResourceIDs[1], diff --git a/test/contractBridge/depositXC20.js b/test/contractBridge/depositXC20.js index 228a3ff8..bb10493f 100644 --- a/test/contractBridge/depositXC20.js +++ b/test/contractBridge/depositXC20.js @@ -224,7 +224,7 @@ contract("Bridge - [deposit - XRC20]", async (accounts) => { }); it("should if XC20Safe contract call fails", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, initialResourceIDs[1], diff --git a/test/contractBridge/executeProposalERC20.js b/test/contractBridge/executeProposalERC20.js index b5f1a4e8..9fa132e0 100644 --- a/test/contractBridge/executeProposalERC20.js +++ b/test/contractBridge/executeProposalERC20.js @@ -132,7 +132,7 @@ contract("Bridge - [execute proposal - ERC20]", async (accounts) => { }) ); - await TruffleAssert.passes( + await Helpers.passes( BridgeInstance.executeProposal(proposal, proposalSignedData, { from: relayer1Address, }) diff --git a/test/e2e/erc1155/differentChainsMock.js b/test/e2e/erc1155/differentChainsMock.js index 36bf5525..8da668a9 100644 --- a/test/e2e/erc1155/differentChainsMock.js +++ b/test/e2e/erc1155/differentChainsMock.js @@ -107,13 +107,13 @@ contract("E2E ERC1155 - Two EVM Chains", async (accounts) => { await DestinationERC1155MintableInstance.MINTER_ROLE(), DestinationERC1155HandlerInstance.address ), - OriginBridgeInstance.adminSetResource( + await OriginBridgeInstance.adminSetResource( OriginERC1155HandlerInstance.address, originResourceID, OriginERC1155MintableInstance.address, emptySetResourceData ), - DestinationBridgeInstance.adminSetResource( + await DestinationBridgeInstance.adminSetResource( DestinationERC1155HandlerInstance.address, destinationResourceID, DestinationERC1155MintableInstance.address, diff --git a/test/e2e/erc1155/sameChain.js b/test/e2e/erc1155/sameChain.js index 07aa16ff..818fd905 100644 --- a/test/e2e/erc1155/sameChain.js +++ b/test/e2e/erc1155/sameChain.js @@ -150,7 +150,7 @@ contract("E2E ERC1155 - Same Chain", async (accounts) => { }); it("Handler's deposit function can be called by only bridge", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( ERC1155HandlerInstance.deposit( resourceID, depositorAddress, @@ -162,7 +162,7 @@ contract("E2E ERC1155 - Same Chain", async (accounts) => { }); it("Handler's executeProposal function can be called by only bridge", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( ERC1155HandlerInstance.executeProposal(resourceID, proposalData, { from: depositorAddress, }), @@ -179,7 +179,7 @@ contract("E2E ERC1155 - Same Chain", async (accounts) => { "0x" ); - await TruffleAssert.reverts( + await Helpers.reverts( ERC1155HandlerInstance.withdraw(withdrawData, {from: depositorAddress}), "sender must be bridge contract" ); diff --git a/test/e2e/erc721/differentChainsMock.js b/test/e2e/erc721/differentChainsMock.js index c50324ab..f78930bf 100644 --- a/test/e2e/erc721/differentChainsMock.js +++ b/test/e2e/erc721/differentChainsMock.js @@ -101,13 +101,13 @@ contract("E2E ERC721 - Two EVM Chains", async (accounts) => { await DestinationERC721MintableInstance.MINTER_ROLE(), DestinationERC721HandlerInstance.address ), - OriginBridgeInstance.adminSetResource( + await OriginBridgeInstance.adminSetResource( OriginERC721HandlerInstance.address, originResourceID, OriginERC721MintableInstance.address, emptySetResourceData ), - DestinationBridgeInstance.adminSetResource( + await DestinationBridgeInstance.adminSetResource( DestinationERC721HandlerInstance.address, destinationResourceID, DestinationERC721MintableInstance.address, @@ -269,7 +269,7 @@ contract("E2E ERC721 - Two EVM Chains", async (accounts) => { ); // Token should no longer exist - TruffleAssert.reverts( + await Helpers.reverts( DestinationERC721MintableInstance.ownerOf(tokenID), "ERC721: owner query for nonexistent token" ); @@ -284,7 +284,7 @@ contract("E2E ERC721 - Two EVM Chains", async (accounts) => { ); // Assert Destination tokenID no longer exists - TruffleAssert.reverts( + await Helpers.reverts( DestinationERC721MintableInstance.ownerOf(tokenID), "ERC721: owner query for nonexistent token" ); diff --git a/test/forwarder/forwarder.js b/test/forwarder/forwarder.js index e86dbc3b..6e54b8f8 100644 --- a/test/forwarder/forwarder.js +++ b/test/forwarder/forwarder.js @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only const TruffleAssert = require("truffle-assertions"); +const Helpers = require("../helpers"); const Ethers = require("ethers"); const Wallet = require("ethereumjs-wallet").default; const ethSigUtil = require("eth-sig-util"); @@ -104,7 +105,7 @@ contract("Forwarder", async () => { }; assert.equal(await ForwarderInstance.verify(request_other, sign), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign), "MinimalForwarder: signature does not match request" ); @@ -122,7 +123,7 @@ contract("Forwarder", async () => { }; assert.equal(await ForwarderInstance.verify(request_other, sign), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign), "MinimalForwarder: signature does not match request" ); @@ -140,7 +141,7 @@ contract("Forwarder", async () => { }; assert.equal(await ForwarderInstance.verify(request_other, sign), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign), "MinimalForwarder: signature does not match request" ); @@ -158,7 +159,7 @@ contract("Forwarder", async () => { }; assert.equal(await ForwarderInstance.verify(request_other, sign), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign), "MinimalForwarder: signature does not match request" ); @@ -176,7 +177,7 @@ contract("Forwarder", async () => { }; assert.equal(await ForwarderInstance.verify(request_other, sign), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign), "MinimalForwarder: signature does not match request" ); @@ -194,7 +195,7 @@ contract("Forwarder", async () => { }; assert.equal(await ForwarderInstance.verify(request_other, sign), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign), "MinimalForwarder: signature does not match request" ); @@ -211,7 +212,7 @@ contract("Forwarder", async () => { }); assert.equal(await ForwarderInstance.verify(request, sign_other), false); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request, sign_other), "MinimalForwarder: signature does not match request" ); @@ -241,7 +242,7 @@ contract("Forwarder", async () => { await ForwarderInstance.verify(request_other, sign_other), false ); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request_other, sign_other), "MinimalForwarder: signature does not match request" ); @@ -379,7 +380,7 @@ contract("Forwarder", async () => { it("The successful execute can not be replayed again", async () => { await ForwarderInstance.execute(request, sign); - return TruffleAssert.reverts( + return Helpers.reverts( ForwarderInstance.execute(request, sign), "MinimalForwarder: signature does not match request" ); diff --git a/test/frostKeygen/frostKeygen.js b/test/frostKeygen/frostKeygen.js index bd6002da..6fb2d235 100644 --- a/test/frostKeygen/frostKeygen.js +++ b/test/frostKeygen/frostKeygen.js @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only const TruffleAssert = require("truffle-assertions"); +const Helpers = require("../helpers"); const FROSTKeygen = artifacts.require("FROSTKeygen") contract("FROSTKeygen", (accounts) => { @@ -21,7 +22,7 @@ contract("FROSTKeygen", (accounts) => { it("should revert when startFROSTKeygen is not called by the owner", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( FROSTKeygenInstance.startFROSTKeygen({from: accounts[1]}), ) @@ -33,7 +34,7 @@ contract("FROSTKeygen", (accounts) => { TruffleAssert.eventEmitted(tx, "StartedFROSTKeygen"); - await TruffleAssert.reverts( + await Helpers.reverts( FROSTKeygenInstance.startFROSTKeygen({from: accounts[0]})) }) }) diff --git a/test/gasBenchmarks/deployments.js b/test/gasBenchmarks/deployments.js index 259fa7fc..76b46c57 100644 --- a/test/gasBenchmarks/deployments.js +++ b/test/gasBenchmarks/deployments.js @@ -1,7 +1,7 @@ // The Licensed Work is (c) 2022 Sygma // SPDX-License-Identifier: LGPL-3.0-only -const Helpers = require("../../test/helpers"); +const Helpers = require("../helpers"); const BridgeContract = artifacts.require("Bridge"); const AccessControlSegregatorContract = artifacts.require( @@ -26,7 +26,7 @@ contract("Gas Benchmark - [contract deployments]", async (accounts) => { let BridgeInstance; - it("Should deploy all contracts and print benchmarks", async () => { + it.skip("Should deploy all contracts and print benchmarks", async () => { const accessControlInstance = await AccessControlSegregatorContract.new( Helpers.accessControlFuncSignatures, Array(13).fill(accounts[0]) diff --git a/test/handlers/erc1155/depositBurn.js b/test/handlers/erc1155/depositBurn.js index 2a1a6e48..593bc1da 100644 --- a/test/handlers/erc1155/depositBurn.js +++ b/test/handlers/erc1155/depositBurn.js @@ -74,13 +74,13 @@ contract("ERC1155Handler - [Deposit Burn ERC1155]", async (accounts) => { true, {from: depositorAddress} ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC1155HandlerInstance.address, resourceID1, ERC1155MintableInstance1.address, emptySetResourceData ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC1155HandlerInstance.address, resourceID2, ERC1155MintableInstance2.address, diff --git a/test/handlers/erc20/deposit.js b/test/handlers/erc20/deposit.js index fe01ee3d..875a47b6 100644 --- a/test/handlers/erc20/deposit.js +++ b/test/handlers/erc20/deposit.js @@ -175,7 +175,7 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { const recipientAddress = accounts[0] + accounts[1].substr(2); const lenRecipientAddress = 40; - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID_ZERO_Address, @@ -190,7 +190,7 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { "ERC20: not a contract" ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID_EOA_Address, diff --git a/test/handlers/erc20/depositBurn.js b/test/handlers/erc20/depositBurn.js index 1a7f305a..5781724d 100644 --- a/test/handlers/erc20/depositBurn.js +++ b/test/handlers/erc20/depositBurn.js @@ -64,13 +64,13 @@ contract("ERC20Handler - [Deposit Burn ERC20]", async (accounts) => { depositAmount, {from: depositorAddress} ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC20HandlerInstance.address, resourceID1, ERC20MintableInstance1.address, emptySetResourceData ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC20HandlerInstance.address, resourceID2, ERC20MintableInstance2.address, diff --git a/test/handlers/erc721/depositBurn.js b/test/handlers/erc721/depositBurn.js index 10db497e..013f7c87 100644 --- a/test/handlers/erc721/depositBurn.js +++ b/test/handlers/erc721/depositBurn.js @@ -69,13 +69,13 @@ contract("ERC721Handler - [Deposit Burn ERC721]", async (accounts) => { ERC721MintableInstance1.approve(ERC721HandlerInstance.address, tokenID, { from: depositorAddress, }), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC721HandlerInstance.address, resourceID1, ERC721MintableInstance1.address, emptySetResourceData ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC721HandlerInstance.address, resourceID2, ERC721MintableInstance2.address, @@ -127,14 +127,14 @@ contract("ERC721Handler - [Deposit Burn ERC721]", async (accounts) => { ); assert.strictEqual(depositorBalance.toNumber(), 0); - await TruffleAssert.reverts( + await Helpers.reverts( ERC721MintableInstance1.ownerOf(tokenID), "ERC721: owner query for nonexistent token" ); }); it("depositAmount of ERC721MintableInstance1 tokens should NOT burn from NOT token owner", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID1, diff --git a/test/handlers/fee/basic/admin.js b/test/handlers/fee/basic/admin.js index b6079c95..fdbede01 100644 --- a/test/handlers/fee/basic/admin.js +++ b/test/handlers/fee/basic/admin.js @@ -16,7 +16,7 @@ contract("BasicFeeHandler - [admin]", async (accounts) => { const currentFeeHandlerAdmin = accounts[0]; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: initialRelayers[1]}), "sender doesn't have admin role" ); diff --git a/test/handlers/fee/basic/changeFee.js b/test/handlers/fee/basic/changeFee.js index 9d81526a..dc3149a2 100644 --- a/test/handlers/fee/basic/changeFee.js +++ b/test/handlers/fee/basic/changeFee.js @@ -16,7 +16,7 @@ contract("BasicFeeHandler - [changeFee]", async (accounts) => { const nonAdmin = accounts[1]; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: nonAdmin}), "sender doesn't have admin role" ); @@ -70,7 +70,7 @@ contract("BasicFeeHandler - [changeFee]", async (accounts) => { BridgeInstance.address, FeeHandlerRouterInstance.address ); - await TruffleAssert.reverts( + await Helpers.reverts( BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, 0), "Current fee is equal to new fee" ); diff --git a/test/handlers/fee/basic/collectFee.js b/test/handlers/fee/basic/collectFee.js index 36395694..e1356fdf 100644 --- a/test/handlers/fee/basic/collectFee.js +++ b/test/handlers/fee/basic/collectFee.js @@ -82,25 +82,25 @@ contract("BasicFeeHandler - [collectFee]", async (accounts) => { ); await Promise.all([ - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC20HandlerInstance.address, erc20ResourceID, ERC20MintableInstance.address, emptySetResourceData ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( ERC721HandlerInstance.address, erc721ResourceID, ERC721MintableInstance.address, emptySetResourceData ), - ERC20MintableInstance.mint(depositorAddress, depositAmount), + await ERC20MintableInstance.mint(depositorAddress, depositAmount), ERC20MintableInstance.approve( ERC20HandlerInstance.address, depositAmount, {from: depositorAddress} ), - ERC721MintableInstance.mint(depositorAddress, tokenID, ""), + await ERC721MintableInstance.mint(depositorAddress, tokenID, ""), ERC721MintableInstance.approve(ERC721HandlerInstance.address, tokenID, { from: depositorAddress, }), @@ -281,7 +281,7 @@ contract("BasicFeeHandler - [collectFee]", async (accounts) => { "0x0000000000000000000000000000000000000000" ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, erc20ResourceID, @@ -329,7 +329,7 @@ contract("BasicFeeHandler - [collectFee]", async (accounts) => { ERC20BasicFeeHandlerInstance.address ); - await TruffleAssert.reverts( + await Helpers.reverts( ERC20BasicFeeHandlerInstance.collectFee( depositorAddress, originDomainID, @@ -372,7 +372,7 @@ contract("BasicFeeHandler - [collectFee]", async (accounts) => { ERC20BasicFeeHandlerInstance.address ); - await TruffleAssert.reverts( + await Helpers.reverts( FeeHandlerRouterInstance.collectFee( depositorAddress, originDomainID, diff --git a/test/handlers/fee/basic/distributeFee.js b/test/handlers/fee/basic/distributeFee.js index b12a2975..f74b130d 100644 --- a/test/handlers/fee/basic/distributeFee.js +++ b/test/handlers/fee/basic/distributeFee.js @@ -23,7 +23,7 @@ contract("BasicFeeHandler - [distributeFee]", async (accounts) => { const emptySetResourceData = "0x"; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: accounts[1]}), "sender doesn't have admin role" ); @@ -211,7 +211,7 @@ contract("BasicFeeHandler - [distributeFee]", async (accounts) => { ); const payout = Ethers.utils.parseEther("0.5"); - await TruffleAssert.reverts( + await Helpers.reverts( BasicFeeHandlerInstance.transferFee( [accounts[3], accounts[4]], [payout, payout, payout] diff --git a/test/handlers/fee/handlerRouter.js b/test/handlers/fee/handlerRouter.js index 822b1587..0fcdf6f4 100644 --- a/test/handlers/fee/handlerRouter.js +++ b/test/handlers/fee/handlerRouter.js @@ -22,7 +22,7 @@ contract("FeeHandlerRouter", async (accounts) => { const bridgeAddress = accounts[4]; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: nonAdmin}), "sender doesn't have admin role" ); @@ -171,7 +171,7 @@ contract("FeeHandlerRouter", async (accounts) => { const depositData = Helpers.createERCDepositData(100, 20, recipientAddress); await Helpers.expectToRevertWithCustomError( - FeeHandlerRouterInstance.collectFee( + FeeHandlerRouterInstance.collectFee.call( whitelistAddress, originDomainID, destinationDomainID, @@ -185,7 +185,7 @@ contract("FeeHandlerRouter", async (accounts) => { ), "IncorrectFeeSupplied(uint256)" ); - await TruffleAssert.passes( + await Helpers.passes( FeeHandlerRouterInstance.collectFee( nonWhitelistAddress, originDomainID, diff --git a/test/handlers/fee/percentage/admin.js b/test/handlers/fee/percentage/admin.js index 4ce3b68c..0778858a 100644 --- a/test/handlers/fee/percentage/admin.js +++ b/test/handlers/fee/percentage/admin.js @@ -17,7 +17,7 @@ contract("PercentageFeeHandler - [admin]", async (accounts) => { const currentFeeHandlerAdmin = accounts[0]; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: initialRelayers[1]}), "sender doesn't have admin role" ); diff --git a/test/handlers/fee/percentage/changeFee.js b/test/handlers/fee/percentage/changeFee.js index efc8bbc6..55baea41 100644 --- a/test/handlers/fee/percentage/changeFee.js +++ b/test/handlers/fee/percentage/changeFee.js @@ -19,7 +19,7 @@ contract("PercentageFeeHandler - [change fee and bounds]", async (accounts) => { let resourceID; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: nonAdmin}), "sender doesn't have admin role" ); @@ -80,7 +80,7 @@ contract("PercentageFeeHandler - [change fee and bounds]", async (accounts) => { BridgeInstance.address, FeeHandlerRouterInstance.address ); - await TruffleAssert.reverts( + await Helpers.reverts( PercentageFeeHandlerInstance.changeFee(destinationDomainID, resourceID, 0), "Current fee is equal to new fee" ); @@ -120,7 +120,7 @@ contract("PercentageFeeHandler - [change fee and bounds]", async (accounts) => { FeeHandlerRouterInstance.address ); await PercentageFeeHandlerInstance.changeFeeBounds(resourceID, 25, 50) - await TruffleAssert.reverts( + await Helpers.reverts( PercentageFeeHandlerInstance.changeFeeBounds(resourceID, 25, 50), "Current bounds are equal to new bounds" ); @@ -131,7 +131,7 @@ contract("PercentageFeeHandler - [change fee and bounds]", async (accounts) => { BridgeInstance.address, FeeHandlerRouterInstance.address ); - await TruffleAssert.reverts( + await Helpers.reverts( PercentageFeeHandlerInstance.changeFeeBounds(resourceID, 50, 25), "Upper bound must be larger than lower bound or 0" ); diff --git a/test/handlers/fee/percentage/collectFee.js b/test/handlers/fee/percentage/collectFee.js index 63ed09ae..1e182748 100644 --- a/test/handlers/fee/percentage/collectFee.js +++ b/test/handlers/fee/percentage/collectFee.js @@ -144,7 +144,7 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { }); it("deposit should revert if msg.value != 0", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID, @@ -155,7 +155,7 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { value: Ethers.utils.parseEther("0.5").toString(), } ), - "msg.value != 0" + "collectFee: msg.value != 0" ); }); @@ -171,7 +171,7 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { 0, {from: depositorAddress} ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID, @@ -196,7 +196,7 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { 0, {from: depositorAddress} ); - await TruffleAssert.reverts( + await Helpers.reverts( PercentageFeeHandlerInstance.collectFee( depositorAddress, originDomainID, @@ -224,7 +224,7 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { 0, {from: depositorAddress} ); - await TruffleAssert.reverts( + await Helpers.reverts( FeeHandlerRouterInstance.collectFee( depositorAddress, originDomainID, diff --git a/test/handlers/fee/percentage/distributeFee.js b/test/handlers/fee/percentage/distributeFee.js index 21932211..74b528ca 100644 --- a/test/handlers/fee/percentage/distributeFee.js +++ b/test/handlers/fee/percentage/distributeFee.js @@ -33,7 +33,7 @@ contract("PercentageFeeHandler - [distributeFee]", async (accounts) => { let depositData; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: accounts[1]}), "sender doesn't have admin role" ); @@ -169,16 +169,15 @@ contract("PercentageFeeHandler - [distributeFee]", async (accounts) => { ); assert.equal(balance, feeAmount); - // Incorrect resourceID - resourceID = Helpers.createResourceID( + const incorrectResourceID = Helpers.createResourceID( PercentageFeeHandlerInstance.address, originDomainID ); // Transfer the funds: fails - await TruffleAssert.reverts( + await Helpers.reverts( PercentageFeeHandlerInstance.transferERC20Fee( - resourceID, + incorrectResourceID, [accounts[3], accounts[4]], [payout, payout] ) @@ -227,7 +226,7 @@ contract("PercentageFeeHandler - [distributeFee]", async (accounts) => { ); assert.equal(balance, feeAmount); - await TruffleAssert.reverts( + await Helpers.reverts( PercentageFeeHandlerInstance.transferERC20Fee( resourceID, [accounts[3], accounts[4]], diff --git a/test/handlers/generic/permissionlessDeposit.js b/test/handlers/generic/permissionlessDeposit.js index 12556164..9b9f9ef5 100644 --- a/test/handlers/generic/permissionlessDeposit.js +++ b/test/handlers/generic/permissionlessDeposit.js @@ -118,7 +118,7 @@ contract("GmpHandler - [deposit]", async (accounts) => { // Min length is 76 bytes const invalidDepositData = "0x" + "aa".repeat(75); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID, @@ -141,7 +141,7 @@ contract("GmpHandler - [deposit]", async (accounts) => { hashOfTestStore ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID, @@ -164,7 +164,7 @@ contract("GmpHandler - [deposit]", async (accounts) => { hashOfTestStore ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID, diff --git a/test/handlers/xc20/deposit.js b/test/handlers/xc20/deposit.js index d21fb1db..c7084ec2 100644 --- a/test/handlers/xc20/deposit.js +++ b/test/handlers/xc20/deposit.js @@ -175,7 +175,7 @@ contract("XC20Handler - [Deposit ERC20]", async (accounts) => { const recipientAddress = accounts[0] + accounts[1].substr(2); const lenRecipientAddress = 40; - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID_ZERO_Address, @@ -190,7 +190,7 @@ contract("XC20Handler - [Deposit ERC20]", async (accounts) => { "ERC20: not a contract" ); - await TruffleAssert.reverts( + await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, resourceID_EOA_Address, diff --git a/test/handlers/xc20/depositBurn.js b/test/handlers/xc20/depositBurn.js index 54aceb86..14e01298 100644 --- a/test/handlers/xc20/depositBurn.js +++ b/test/handlers/xc20/depositBurn.js @@ -64,13 +64,13 @@ contract("XC20Handler - [Deposit Burn XC20]", async (accounts) => { depositAmount, {from: depositorAddress} ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( XC20HandlerInstance.address, resourceID1, ERC20MintableInstance1.address, emptySetResourceData ), - BridgeInstance.adminSetResource( + await BridgeInstance.adminSetResource( XC20HandlerInstance.address, resourceID2, ERC20MintableInstance2.address, diff --git a/test/helpers.js b/test/helpers.js index fb8e5202..112a2f69 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -358,13 +358,7 @@ const expectToRevertWithCustomError = async function(promise, expectedErrorSigna await promise; } catch (error) { const encoded = web3.eth.abi.encodeFunctionSignature(expectedErrorSignature); - const returnValue = Object.entries(error.data).filter( - (it) => it.length > 1 - ).map( - (it) => it[1] - ).find( - (it) => it != null && it.constructor.name === "Object" && "return" in it - ).return; + const returnValue = error.data.result || error.data; // expect event error and provided error signatures to match assert.equal(returnValue.slice(0, 10), encoded); @@ -380,7 +374,28 @@ const expectToRevertWithCustomError = async function(promise, expectedErrorSigna } return inputParams; } - assert.fail("Expected an exception but none was received"); + assert.fail("Expected a custom error but none was received"); +} + +const reverts = async function(promise, expectedErrorMessage) { + try { + await promise; + } catch (error) { + if (expectedErrorMessage) { + const message = error.reason || error.hijackedStack.split('revert ')[1].split('\n')[0]; + assert.equal(message, expectedErrorMessage); + } + return true; + } + assert.fail("Expected an error message but none was received"); +} + +const passes = async function(promise) { + try { + await promise; + } catch (error) { + assert.fail("Revert reason: " + error.data.result); + } } module.exports = { @@ -413,5 +428,7 @@ module.exports = { mockSignTypedProposalWithInvalidChainID, createDepositProposalDataFromHandlerResponse, createGmpExecutionData, - expectToRevertWithCustomError + expectToRevertWithCustomError, + reverts, + passes, }; diff --git a/test/utils/accessControlSegregator/constructor.js b/test/utils/accessControlSegregator/constructor.js index ac857be6..ebae6f52 100644 --- a/test/utils/accessControlSegregator/constructor.js +++ b/test/utils/accessControlSegregator/constructor.js @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only const TruffleAssert = require("truffle-assertions"); +const Helpers = require("../../helpers"); const AccessControlSegregatorContract = artifacts.require( "AccessControlSegregator" @@ -36,7 +37,7 @@ contract("AccessControlSegregator - [constructor]", async (accounts) => { }); it("should revert if length of functions and accounts array is different", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( AccessControlSegregatorContract.new( ["0xa973ec93", "0x78728c73"], [accounts[0]] diff --git a/test/utils/accessControlSegregator/grantAccess.js b/test/utils/accessControlSegregator/grantAccess.js index 1a9dabcf..fbcce18c 100644 --- a/test/utils/accessControlSegregator/grantAccess.js +++ b/test/utils/accessControlSegregator/grantAccess.js @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only const TruffleAssert = require("truffle-assertions"); +const Helpers = require("../../helpers"); const AccessControlSegregatorContract = artifacts.require( "AccessControlSegregator" @@ -29,7 +30,7 @@ contract("AccessControlSegregator - [grant access]", async (accounts) => { }); it("should revert if sender doesn't have grant access rights", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( AccessControlSegregatorInstance.grantAccess( functionSignature, accounts[2], diff --git a/truffle-config.js b/truffle-config.js index 5a02e87b..79c8a9c5 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -201,7 +201,7 @@ module.exports = { enabled: true, runs: 200 }, - // evmVersion: "byzantium" + evmVersion: "london" } } } diff --git a/yarn.lock b/yarn.lock index ef7bdd82..3508eb79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1618,6 +1618,13 @@ node-interval-tree "^1.3.3" web3-utils "1.5.3" +"@trufflesuite/bigint-buffer@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.10.tgz#a1d9ca22d3cad1a138b78baaf15543637a3e1692" + integrity sha512-pYIQC5EcMmID74t26GCC67946mgTJFiLXOT/BYozgrd4UEY2JHEGLhWi9cMiQCt5BSqFEvKkCHNnoj82SRjiEw== + dependencies: + node-gyp-build "4.4.0" + "@trufflesuite/bigint-buffer@1.1.9": version "1.1.9" resolved "https://registry.yarnpkg.com/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.9.tgz#e2604d76e1e4747b74376d68f1312f9944d0d75d" @@ -1648,6 +1655,16 @@ websocket "^1.0.31" ws "^7.3.1" +"@trufflesuite/uws-js-unofficial@20.30.0-unofficial.0": + version "20.30.0-unofficial.0" + resolved "https://registry.yarnpkg.com/@trufflesuite/uws-js-unofficial/-/uws-js-unofficial-20.30.0-unofficial.0.tgz#2fbc2f8ef7e82fbeea6abaf7e8a9d42a02b479d3" + integrity sha512-r5X0aOQcuT6pLwTRLD+mPnAM/nlKtvIK4Z+My++A8tTOR0qTjNRx8UB8jzRj3D+p9PMAp5LnpCUUGmz7/TppwA== + dependencies: + ws "8.13.0" + optionalDependencies: + bufferutil "4.0.7" + utf-8-validate "6.0.3" + "@typechain/ethers-v5@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-9.0.0.tgz#6aa93bea7425c0463bd8a61eea3643540ef851bd" @@ -1761,6 +1778,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/lru-cache@5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" + integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -1832,6 +1854,11 @@ dependencies: "@types/node" "*" +"@types/seedrandom@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.1.tgz#1254750a4fec4aff2ebec088ccd0bb02e91fedb4" + integrity sha512-giB9gzDeiCeloIXDgzFBCgjj1k4WxcDrZtGl6h1IqmUPlxF+Nx8Ve+96QCyDZ/HseB/uvDsKbpib9hU5cU53pw== + "@types/serve-static@*": version "1.13.10" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" @@ -1931,18 +1958,20 @@ abort-controller@3.0.0, abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -abstract-leveldown@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" - integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== +abstract-level@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-1.0.3.tgz#78a67d3d84da55ee15201486ab44c09560070741" + integrity sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA== dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" + buffer "^6.0.3" + catering "^2.1.0" + is-buffer "^2.0.5" + level-supports "^4.0.0" + level-transcoder "^1.0.1" + module-error "^1.0.1" + queue-microtask "^1.2.3" -abstract-leveldown@^7.2.0: +abstract-leveldown@7.2.0, abstract-leveldown@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz#08d19d4e26fb5be426f7a57004851b39e1795a2e" integrity sha512-DnhQwcFEaYsvYDnACLZhMmCWd3rkOeEvglpa4q5i/5Jlm3UIsWaxVzuXvDLFCSCWRO3yy2/+V/G7FusFgejnfQ== @@ -1954,6 +1983,17 @@ abstract-leveldown@^7.2.0: level-supports "^2.0.1" queue-microtask "^1.2.3" +abstract-leveldown@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" + integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== + dependencies: + buffer "^5.5.0" + immediate "^3.2.3" + level-concat-iterator "~2.0.0" + level-supports "~1.0.0" + xtend "~4.0.0" + abstract-leveldown@~2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" @@ -2385,7 +2425,7 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== -async-eventemitter@^0.2.2: +async-eventemitter@0.2.4, async-eventemitter@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca" integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw== @@ -2983,6 +3023,13 @@ bufferutil@4.0.5: dependencies: node-gyp-build "^4.3.0" +bufferutil@4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" + integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + dependencies: + node-gyp-build "^4.3.0" + bufferutil@^4.0.1: version "4.0.6" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433" @@ -4927,19 +4974,6 @@ ethereumjs-tx@^2.1.1: ethereumjs-common "^1.5.0" ethereumjs-util "^6.0.0" -ethereumjs-util@6.2.1, ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" - integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw== - dependencies: - "@types/bn.js" "^4.11.3" - bn.js "^4.11.0" - create-hash "^1.1.2" - elliptic "^6.5.2" - ethereum-cryptography "^0.1.3" - ethjs-util "0.1.6" - rlp "^2.2.3" - ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.5: version "5.2.1" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz#a833f0e5fca7e5b361384dc76301a721f537bf65" @@ -4953,6 +4987,19 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" +ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" + integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw== + dependencies: + "@types/bn.js" "^4.11.3" + bn.js "^4.11.0" + create-hash "^1.1.2" + elliptic "^6.5.2" + ethereum-cryptography "^0.1.3" + ethjs-util "0.1.6" + rlp "^2.2.3" + ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz#a6885bcdd92045b06f596c7626c3e89ab3312458" @@ -5569,15 +5616,6 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -ganache-cli@^6.12.2: - version "6.12.2" - resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.2.tgz#c0920f7db0d4ac062ffe2375cb004089806f627a" - integrity sha512-bnmwnJDBDsOWBUP8E/BExWf85TsdDEFelQSzihSJm9VChVO1SHp94YXLP5BlA4j/OTxp0wR4R1Tje9OHOuAJVw== - dependencies: - ethereumjs-util "6.2.1" - source-map-support "0.5.12" - yargs "13.2.4" - ganache@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/ganache/-/ganache-7.0.3.tgz#c90a28b039378d2e1b5d9623b354969a1d877070" @@ -5592,6 +5630,27 @@ ganache@^7.0.3: bufferutil "4.0.5" utf-8-validate "5.0.7" +ganache@^7.9.2: + version "7.9.2" + resolved "https://registry.yarnpkg.com/ganache/-/ganache-7.9.2.tgz#77f506ad2735dd9109696ffa1834a9dd2f806449" + integrity sha512-7gsVVDpO9AhrFyDMWWl7SpMsPpqGcnAzjxz3k32LheIPNd64p2XsY9GYRdhWmKuryb60W1iaWPZWDkFKlbRWHA== + dependencies: + "@trufflesuite/bigint-buffer" "1.1.10" + "@trufflesuite/uws-js-unofficial" "20.30.0-unofficial.0" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "5.1.1" + "@types/seedrandom" "3.0.1" + abstract-level "1.0.3" + abstract-leveldown "7.2.0" + async-eventemitter "0.2.4" + emittery "0.10.0" + keccak "3.0.2" + leveldown "6.1.0" + secp256k1 "4.0.3" + optionalDependencies: + bufferutil "4.0.5" + utf-8-validate "5.0.7" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -6250,11 +6309,6 @@ invariant@^2.2.2: dependencies: loose-envify "^1.0.0" -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - ip-regex@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" @@ -7082,7 +7136,7 @@ keccak@3.0.1: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" -keccak@^3.0.0: +keccak@3.0.2, keccak@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== @@ -7108,13 +7162,6 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - lcov-parse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" @@ -7221,6 +7268,11 @@ level-supports@^2.0.1: resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-2.1.0.tgz#9af908d853597ecd592293b2fad124375be79c5f" integrity sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA== +level-supports@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-4.0.1.tgz#431546f9d81f10ff0fea0e74533a0e875c08c66a" + integrity sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA== + level-supports@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" @@ -7228,6 +7280,14 @@ level-supports@~1.0.0: dependencies: xtend "^4.0.2" +level-transcoder@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c" + integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== + dependencies: + buffer "^6.0.3" + module-error "^1.0.1" + level-write-stream@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/level-write-stream/-/level-write-stream-1.0.0.tgz#3f7fbb679a55137c0feb303dee766e12ee13c1dc" @@ -7542,13 +7602,6 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - marked-terminal@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-3.3.0.tgz#25ce0c0299285998c7636beaefc87055341ba1bd" @@ -7585,15 +7638,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - memdown@1.4.1, memdown@^1.0.0: version "1.4.1" resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" @@ -7680,7 +7724,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -7830,6 +7874,11 @@ mock-fs@^4.1.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== +module-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" + integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -8111,6 +8160,11 @@ node-gyp-build@4.3.0, node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" + integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ== + node-gyp-build@~3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.8.0.tgz#0f57efeb1971f404dfcbfab975c284de7c70f14a" @@ -8407,15 +8461,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -8439,11 +8484,6 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-defer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83" @@ -8462,11 +8502,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -9807,7 +9842,7 @@ secp256k1@4.0.2: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" -secp256k1@^4.0.0, secp256k1@^4.0.1: +secp256k1@4.0.3, secp256k1@^4.0.0, secp256k1@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== @@ -10093,14 +10128,6 @@ solmate@^6.2.0: resolved "https://registry.yarnpkg.com/solmate/-/solmate-6.2.0.tgz#edd29b5f3d6faafafdcf65fe4d1d959b4841cfa8" integrity sha512-AM38ioQ2P8zRsA42zenb9or6OybRjOLXIu3lhIT8rhddUuduCt76pUEuLxOIg9GByGojGz+EbpFdCB6B+QZVVA== -source-map-support@0.5.12: - version "0.5.12" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" - integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@^0.5.17, source-map-support@^0.5.3: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -11015,6 +11042,13 @@ utf-8-validate@5.0.7: dependencies: node-gyp-build "^4.3.0" +utf-8-validate@6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" + integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA== + dependencies: + node-gyp-build "^4.3.0" + utf-8-validate@^5.0.2: version "5.0.9" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.9.tgz#ba16a822fbeedff1a58918f2a6a6b36387493ea3" @@ -11580,6 +11614,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" @@ -11693,7 +11732,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@13.1.2, yargs-parser@^13.1.0, yargs-parser@^13.1.2: +yargs-parser@13.1.2, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -11725,23 +11764,6 @@ yargs-unparser@1.6.1: is-plain-obj "^1.1.0" yargs "^14.2.3" -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yargs@13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" From 48428e3fbab4fd68a5d27d47e8c8c741a7c81aea Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Sun, 4 Aug 2024 18:18:53 +0200 Subject: [PATCH 06/14] update to ganache --- .gitignore | 2 +- Makefile | 2 +- package.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 795262a9..3427f008 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ coverage/ coverage.json src/ethers src/web3 -ganache-cli/ +ganache/ dist/ data/ .vscode/ diff --git a/Makefile b/Makefile index 8e0f4468..8f137ad2 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ start-ganache: start-forkedMainnet: @echo " > \033[32mStarting forked environment... \033[0m " - ganache-cli -f $(FORKED_TESTS_PROVIDER) & sleep 3 + ganache -f $(FORKED_TESTS_PROVIDER) & sleep 3 start-geth: @echo " > \033[32mStarting geth... \033[0m " diff --git a/package.json b/package.json index dc651034..3e2aabf6 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "generate-types": "npm run generate-types:ethers", "generate-types:ethers": "npx typechain \"build/contracts/*\" --target=ethers-v5 --out-dir src/ethers", "test": "echo \\\\\\\"Error: no test specified\\\\\\\" && exit 1", - "deploy:local:1": "concurrently --raw --kill-others --success first \"ganache-cli --chainId 1337 -d --db data/ --m 'black toward wish jar twin produce remember fluid always confirm bacon slush' \" \"truffle migrate --network test\"", - "deploy:local:2": "concurrently --raw --kill-others --success first \"ganache-cli -p 8547 --chainId 1338 -d --db data/ --m 'black toward wish jar twin produce remember fluid always confirm bacon slush' \" \"truffle migrate --network test2\"", + "deploy:local:1": "concurrently --raw --kill-others --success first \"ganache --chainId 1337 -d --db data/ --m 'black toward wish jar twin produce remember fluid always confirm bacon slush' \" \"truffle migrate --network test\"", + "deploy:local:2": "concurrently --raw --kill-others --success first \"ganache -p 8547 --chainId 1338 -d --db data/ --m 'black toward wish jar twin produce remember fluid always confirm bacon slush' \" \"truffle migrate --network test2\"", "lint": "npm run lint:solidity && npm run lint:js", "lint:solidity": "solhint contracts/**/*.sol", "lint:js": "eslint --ext .js ." From 606da56d17b80942a1a5809b64a8548d7cbf55d4 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Mon, 5 Aug 2024 17:30:02 +0700 Subject: [PATCH 07/14] Fix forked tests --- .github/workflows/test.yml | 2 +- Makefile | 4 ++++ testUnderForked/admin.js | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ed27d9b..c38836e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: run: | fuser -k 8545/tcp make start-forkedMainnet FORKED_TESTS_PROVIDER=${{ secrets.FORKED_TESTS_PROVIDER }} - npx truffle test testUnderForked/* + make test-forked coverage: needs: test diff --git a/Makefile b/Makefile index 8f137ad2..0c6358fb 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ start-forkedMainnet: @echo " > \033[32mStarting forked environment... \033[0m " ganache -f $(FORKED_TESTS_PROVIDER) & sleep 3 +test-forked: + @echo " > \033[32mTesting contracts... \033[0m " + truffle test --stacktrace testUnderForked/* + start-geth: @echo " > \033[32mStarting geth... \033[0m " ./scripts/geth/start_geth.sh diff --git a/testUnderForked/admin.js b/testUnderForked/admin.js index 50c26789..0baf3de9 100644 --- a/testUnderForked/admin.js +++ b/testUnderForked/admin.js @@ -31,7 +31,7 @@ contract("TwapFeeHandler - [admin]", async (accounts) => { const currentFeeHandlerAdmin = accounts[0]; const assertOnlyAdmin = (method, ...params) => { - return TruffleAssert.reverts( + return Helpers.reverts( method(...params, {from: initialRelayers[1]}), "sender doesn't have admin role" ); @@ -198,7 +198,7 @@ contract("TwapFeeHandler - [admin]", async (accounts) => { ) ); - await TruffleAssert.passes( + await Helpers.passes( DynamicFeeHandlerInstance.renounceAdmin( expectedDynamicFeeHandlerAdmin ) From 319e46ebe060a51073f61edc4ac30363fa51e704 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Mon, 5 Aug 2024 21:43:51 +0200 Subject: [PATCH 08/14] temp remove coverage from CI --- .github/workflows/test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c38836e0..a93b7458 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,9 +64,3 @@ jobs: path: build - name: Yarn install run: yarn install --frozen-lockfile - - name: Run coverage - run: ./node_modules/.bin/truffle run coverage -solcoverjs ./scripts/.solcover.js --network test - - name: Coverall - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} From dde972d65f686842b70b179fcdf7af6d3f1e62e9 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Wed, 7 Aug 2024 16:54:01 +0200 Subject: [PATCH 09/14] fix data encoding, add withdraw method and src-dest token mapping --- contracts/adapters/GmpTransferAdapter.sol | 49 ++++++++++++++++--- .../interfaces/IGmpTransferAdapter.sol | 37 ++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 contracts/adapters/interfaces/IGmpTransferAdapter.sol diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol index 1d32b81e..07625ecb 100644 --- a/contracts/adapters/GmpTransferAdapter.sol +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -2,23 +2,30 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.11; +import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import "../../contracts/interfaces/IBridge.sol"; import "../../contracts/interfaces/IFeeHandler.sol"; import "../XERC20/interfaces/IXERC20.sol"; +import "./interfaces/IGmpTransferAdapter.sol"; -contract GmpTransferAdapter { +contract GmpTransferAdapter is IGmpTransferAdapter, AccessControl { using ERC165Checker for address; IBridge public immutable _bridge; bytes32 public immutable _resourceID; address immutable _gmpAddress; + // source token address => destination domainID => destination token address + mapping(address => mapping(uint256 => address)) public crossChainTokenPairs; + + event Withdrawal(address recipient, uint amount); error InsufficientMsgValueAmount(uint256 amount); error InvalidHandler(address handler); error InvalidOriginAdapter(address adapter); - error FailedRefund(); + error CallerNotAdmin(); + error FailedFundsTransfer(); /** @notice This contract requires for transfer that the origin adapter address is the same across all networks. @@ -28,6 +35,12 @@ contract GmpTransferAdapter { _bridge = bridge; _gmpAddress = newGmpAddress; _resourceID = resourceID; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + modifier onlyAdmin() { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert CallerNotAdmin(); + _; } function deposit(uint8 destinationDomainID, address recipientAddress, address XERC20Address, uint256 tokenAmount) external payable { @@ -49,23 +62,31 @@ contract GmpTransferAdapter { if (!success) revert FailedRefund(); } + address destinationToken; + address assignedDestinationToken = crossChainTokenPairs[XERC20Address][destinationDomainID]; + if (assignedDestinationToken != address(0)) { + destinationToken = assignedDestinationToken; + } else { + destinationToken = XERC20Address; + } + bytes memory depositData = abi.encodePacked( // uint256 maxFee uint256(950000), // uint16 len(executeFuncSignature) uint16(4), // bytes executeFuncSignature - IXERC20(address(0)).mint.selector, + IGmpTransferAdapter(address(this)).executeProposal.selector, // uint8 len(executeContractAddress) uint8(20), // bytes executeContractAddress - XERC20Address, + address(this), // uint8 len(executionDataDepositor) uint8(20), // bytes executionDataDepositor address(this), // bytes executionDataDepositor + executionData - prepareDepositData(recipientAddress, XERC20Address, tokenAmount) + prepareDepositData(recipientAddress, destinationToken, tokenAmount) ); IXERC20(XERC20Address).burn(msg.sender, tokenAmount); @@ -80,6 +101,21 @@ contract GmpTransferAdapter { IXERC20(XERC20Address).mint(recipient, amount); } + /** + @notice Used to manually transfer native tokens from Adapter. + @param recipient Address that should recieve the native tokens. + recipient address + */ + function withdraw(address recipient, uint256 amount) external onlyAdmin { + (bool success, ) = address(recipient).call{value: amount}(""); + if(!success) revert FailedFundsTransfer(); + emit Withdrawal(recipient, amount); + } + + function setTokenPairAddress(address sourceTokenAddress, uint8 destinationDomainID, address destinationTokenAddress) external onlyAdmin { + crossChainTokenPairs[sourceTokenAddress][destinationDomainID] = destinationTokenAddress; + } + function slice(bytes calldata input, uint256 position) public pure returns (bytes memory) { return input[position:]; } @@ -89,8 +125,7 @@ contract GmpTransferAdapter { address XERC20Address, uint256 bridgingAmount ) public view returns (bytes memory) { - bytes memory encoded = abi.encode(address(0), recipientAddress, XERC20Address, bridgingAmount); - return this.slice(encoded, 32); + return abi.encode(recipientAddress, XERC20Address, bridgingAmount); } receive() external payable {} diff --git a/contracts/adapters/interfaces/IGmpTransferAdapter.sol b/contracts/adapters/interfaces/IGmpTransferAdapter.sol new file mode 100644 index 00000000..1014feab --- /dev/null +++ b/contracts/adapters/interfaces/IGmpTransferAdapter.sol @@ -0,0 +1,37 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +/** + @title Interface for Bridge contract. + @author ChainSafe Systems. + */ +interface IGmpTransferAdapter { + /** + @notice Initiates a transfer using Gmp handler. + @param destinationDomainID ID of chain deposit will be bridged to. + @param recipientAddress Address that will receive tokens on destination chain. + @param XERC20Address Address of the tokens that shoul be transferred and burned on source chain. + @param tokenAmount Amount of tokens that should be transferred. + */ + function deposit( + uint8 destinationDomainID, + address recipientAddress, + address XERC20Address, + uint256 tokenAmount + ) external payable; + + /** + @notice Executes a GMP deposit proposal on GMP transfer adapter contract. + @param gmpAdapter Address of the adapter on soruce chain (should be the same address across all chains). + @param recipient Address that will receive tokens. + @param XERC20Address Address of XERC20 contract that will mint tokens on destination chain. + @param amount Amount of tones that should be minted to the recipinet. + */ + function executeProposal( + address gmpAdapter, + address recipient, + address XERC20Address, + uint256 amount + ) external; +} From 206f76288a4936e4f1c2fba2a5ac66f03e938b04 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Wed, 7 Aug 2024 16:55:38 +0200 Subject: [PATCH 10/14] update test after fixes --- .../erc20Transfer/deposit.js | 21 +- .../executeProposalDifferentAddresses.js | 413 ++++++++++++++++++ ...sal.js => executeProposalSameAddresses.js} | 3 +- .../{ => fees}/collectFee.js | 9 +- test/gmpTransferAdapter/fees/withdraw.js | 184 ++++++++ .../nativeTransfer/deposit.js | 21 +- .../nativeTransfer/executeProposal.js | 2 +- 7 files changed, 642 insertions(+), 11 deletions(-) create mode 100644 test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js rename test/gmpTransferAdapter/erc20Transfer/{executeProposal.js => executeProposalSameAddresses.js} (98%) rename test/gmpTransferAdapter/{ => fees}/collectFee.js (96%) create mode 100644 test/gmpTransferAdapter/fees/withdraw.js diff --git a/test/gmpTransferAdapter/erc20Transfer/deposit.js b/test/gmpTransferAdapter/erc20Transfer/deposit.js index cb6769ce..d1dd9c4f 100644 --- a/test/gmpTransferAdapter/erc20Transfer/deposit.js +++ b/test/gmpTransferAdapter/erc20Transfer/deposit.js @@ -17,7 +17,7 @@ const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); -contract("Gmp transfer adapter - [Deposit - ERC20 token]", async (accounts) => { +contract("Gmp transfer adapter - [Deposit XERC20 - wrapped ERC20 token]", async (accounts) => { const originDomainID = 1; const destinationDomainID = 2; const expectedDepositNonce = 1; @@ -25,6 +25,7 @@ contract("Gmp transfer adapter - [Deposit - ERC20 token]", async (accounts) => { const depositorAddress = accounts[1]; const recipientAddress = accounts[3]; + const destinationMaxFee = 950000; const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; const depositAmount = 10; const fee = Ethers.utils.parseEther("0.1"); @@ -173,6 +174,20 @@ contract("Gmp transfer adapter - [Deposit - ERC20 token]", async (accounts) => { }); it("depositEvent is emitted with expected values", async () => { + const preparedExecutionData = await GmpTransferAdapterInstance.prepareDepositData( + recipientAddress, + XERC20Instance.address, + depositAmount + ); + const depositData = Helpers.createGmpDepositData( + depositFunctionSignature, + GmpTransferAdapterInstance.address, + destinationMaxFee, + GmpTransferAdapterInstance.address, + preparedExecutionData, + false + ); + const depositTx = await GmpTransferAdapterInstance.deposit( originDomainID, recipientAddress, @@ -194,7 +209,9 @@ contract("Gmp transfer adapter - [Deposit - ERC20 token]", async (accounts) => { event.destinationDomainID.toNumber() === originDomainID && event.resourceID === resourceID.toLowerCase() && event.depositNonce.toNumber() === expectedDepositNonce && - event.user === GmpTransferAdapterInstance.address + event.user === GmpTransferAdapterInstance.address && + event.data === depositData && + event.handlerResponse == null ); }); }); diff --git a/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js b/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js new file mode 100644 index 00000000..b136f328 --- /dev/null +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js @@ -0,0 +1,413 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); + +const Helpers = require("../../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract(`Gmp transfer adapter - + [Execute proposal XERC20 with different addresses- wrapped ERC20 token]`, async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const recipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const handlerResponseLength = 64; + const contractCallReturndata = Ethers.constants.HashZero; + const destinationMaxFee = 900000; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = 10; + const mintingLimit = 500; + const burningLimit = 500; + + let BridgeInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let sourceXERC20FactoryInstance; + let sourceXERC20Instance; + let sourceXERC20LockboxInstance; + let destinationXERC20FactoryInstance; + let destinationXERC20Instance; + let proposal; + let dataHash; + let depositFunctionSignature; + let ERC20MintableSourceInstance; + + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + adminAddress + )), + ERC20MintableContract.new("sToken", "sTOK").then( + (instance) => (ERC20MintableSourceInstance = instance) + ), + ERC20MintableContract.new("dToken", "dTOK").then( + (instance) => (ERC20MintableDestinationInstance = instance) + ), + ]); + + await ERC20MintableSourceInstance.mint(depositorAddress, depositAmount); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + // deploy source XERC20 contract instances + sourceXERC20FactoryInstance = await XERC20FactoryContract.new(); + const sourceXERC20DeployResponse = await sourceXERC20FactoryInstance.deployXERC20( + "srcSygmaToken", + "srcSTOK", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set source XERC20 contract instance address to the address deployed via XERC20Factory + const sourceDeployedXERC20Address = sourceXERC20DeployResponse.logs[0].args._xerc20 + sourceXERC20Instance = await XERC20Contract.at(sourceDeployedXERC20Address) + const sourceLockboxDeployResponse = await sourceXERC20FactoryInstance.deployLockbox( + sourceXERC20Instance.address, + ERC20MintableSourceInstance.address, + false + ); + // set source Lockbox contract instance address to the address deployed via XERC20Factory + const sourceLockboxAddress = sourceLockboxDeployResponse.logs[0].args._lockbox + sourceXERC20LockboxInstance = await XERC20LockboxContract.at(sourceLockboxAddress); + + // deploy destination contract instances + destinationXERC20FactoryInstance = await XERC20FactoryContract.new(); + const destinationXERC20DeployResponse = await destinationXERC20FactoryInstance.deployXERC20( + "destSygmaToken", + "destSTOK", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set destination XERC20 contract instance address to the address deployed via XERC20Factory + const destinationDeployedXERC20Address = destinationXERC20DeployResponse.logs[0].args._xerc20 + destinationXERC20Instance = await XERC20Contract.at(destinationDeployedXERC20Address) + const destinationLockboxDeployResponse = await destinationXERC20FactoryInstance.deployLockbox( + destinationXERC20Instance.address, + ERC20MintableDestinationInstance.address, + false + ); + // set destination Lockbox contract instance address to the address deployed via XERC20Factory + const destinationLockboxAddress = destinationLockboxDeployResponse.logs[0].args._lockbox + await XERC20LockboxContract.at(destinationLockboxAddress); + + await ERC20MintableSourceInstance.increaseAllowance( + sourceXERC20LockboxInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + await sourceXERC20LockboxInstance.depositTo( + depositorAddress, + depositAmount, + { + from: depositorAddress + } + ); + await sourceXERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + const preparedExecutionData = await GmpTransferAdapterInstance.prepareDepositData( + recipientAddress, + destinationXERC20Instance.address, + transferredAmount + ); + depositData = Helpers.createGmpDepositData( + depositFunctionSignature, + GmpTransferAdapterInstance.address, + destinationMaxFee, + GmpTransferAdapterInstance.address, + preparedExecutionData + ); + + await GmpTransferAdapterInstance.setTokenPairAddress( + sourceXERC20Instance.address, + originDomainID, + destinationXERC20Instance.address + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositData, + }; + + dataHash = Ethers.utils.keccak256( + GmpHandlerInstance.address + depositData.substr(2) + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("isProposalExecuted returns false if depositNonce is not used", async () => { + const destinationDomainID = await BridgeInstance._domainID(); + + assert.isFalse( + await BridgeInstance.isProposalExecuted( + destinationDomainID, + expectedDepositNonce + ) + ); + }); + + it("should create and execute executeProposal successfully", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + const depositorSourceXERC20BalanceBefore = await sourceXERC20Instance.balanceOf(depositorAddress); + const recipientSourceXERC20BalanceBefore = await sourceXERC20Instance.balanceOf(recipientAddress); + const depositorDestinationXERC20BalanceBefore = await destinationXERC20Instance.balanceOf(depositorAddress); + const recipientDestinationXERC20BalanceBefore = await destinationXERC20Instance.balanceOf(recipientAddress); + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + sourceXERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }); + + const recipientSourceNativeBalanceBefore = await web3.eth.getBalance(recipientAddress); + const depositorSourceXERC20BalanceAfter = await sourceXERC20Instance.balanceOf(depositorAddress); + const recipientSourceXERC20BalanceAfter = await sourceXERC20Instance.balanceOf(recipientAddress); + const depositorDestinationXERC20BalanceAfter = await destinationXERC20Instance.balanceOf(depositorAddress); + const recipientDestinationXERC20BalanceAfter = await destinationXERC20Instance.balanceOf(recipientAddress); + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that depositor and recipient balances are aligned with expectations + const recipientNativeBalanceAfter = await web3.eth.getBalance(recipientAddress); + assert.strictEqual(recipientSourceNativeBalanceBefore, recipientNativeBalanceAfter); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).sub(depositorSourceXERC20BalanceBefore.toString()).toString(), + depositorSourceXERC20BalanceAfter.toString() + ); + assert.strictEqual( + recipientSourceXERC20BalanceBefore.toString(), + recipientSourceXERC20BalanceAfter.toString() + ); + assert.strictEqual( + depositorDestinationXERC20BalanceBefore.toString(), + depositorDestinationXERC20BalanceAfter.toString() + ); + assert.strictEqual( + Ethers.BigNumber.from(depositAmount).add(recipientDestinationXERC20BalanceBefore.toString()).toString(), + recipientDestinationXERC20BalanceAfter.toString() + ); + }); + + it("should skip executing proposal if deposit nonce is already used", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + sourceXERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }) + ); + + const skipExecuteTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + // check that no ProposalExecution events are emitted + assert.equal(skipExecuteTx.logs.length, 0); + }); + + it("executeProposal event should be emitted with expected values", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + sourceXERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + const recipientNativeBalanceBefore = await web3.eth.getBalance(recipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["bool", "uint256", "bytes32"], + [true, handlerResponseLength, contractCallReturndata] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + + // check that recipient native token balance hasn't changed + const recipientNativeBalanceAfter = await web3.eth.getBalance(recipientAddress); + assert.strictEqual(recipientNativeBalanceBefore, recipientNativeBalanceAfter); + }); + + it(`should fail to executeProposal if signed Proposal has different + chainID than the one on which it should be executed`, async () => { + const proposalSignedData = + await Helpers.mockSignTypedProposalWithInvalidChainID( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + GmpTransferAdapterInstance.deposit( + originDomainID, + recipientAddress, + sourceXERC20Instance.address, + depositAmount, + { + from: depositorAddress, + value: fee + } + ) + ); + + await Helpers.expectToRevertWithCustomError( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }), + "InvalidProposalSigner()" + ); + }); +}); diff --git a/test/gmpTransferAdapter/erc20Transfer/executeProposal.js b/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js similarity index 98% rename from test/gmpTransferAdapter/erc20Transfer/executeProposal.js rename to test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js index 20b8ec31..58d8251e 100644 --- a/test/gmpTransferAdapter/erc20Transfer/executeProposal.js +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js @@ -17,7 +17,8 @@ const XERC20Contract = artifacts.require("XERC20"); const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); -contract("Gmp transfer adapter - [Execute proposal - ERC20 token]", async (accounts) => { +contract(`Gmp transfer adapter - + [Execute proposal XERC20 with same addresses- wrapped ERC20 token]`, async (accounts) => { const originDomainID = 1; const destinationDomainID = 2; const adminAddress = accounts[0]; diff --git a/test/gmpTransferAdapter/collectFee.js b/test/gmpTransferAdapter/fees/collectFee.js similarity index 96% rename from test/gmpTransferAdapter/collectFee.js rename to test/gmpTransferAdapter/fees/collectFee.js index c05847a9..ed196148 100644 --- a/test/gmpTransferAdapter/collectFee.js +++ b/test/gmpTransferAdapter/fees/collectFee.js @@ -1,9 +1,8 @@ // The Licensed Work is (c) 2022 Sygma // SPDX-License-Identifier: LGPL-3.0-only -const TruffleAssert = require("truffle-assertions"); +const Helpers = require("../../helpers"); const Ethers = require("ethers"); -const Helpers = require("../helpers"); const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); const GmpHandlerContract = artifacts.require( @@ -16,7 +15,7 @@ const XERC20Contract = artifacts.require("XERC20"); const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); -contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => { +contract("Gmp transfer adapter - [Collect fee]", async (accounts) => { const originDomainID = 1; const destinationDomainID = 2; @@ -135,7 +134,7 @@ contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => it("should successfully charge fee on deposit", async () => { const feeHandlerBalanceBefore = await web3.eth.getBalance(BasicFeeHandlerInstance.address); - await TruffleAssert.passes( + await Helpers.passes( GmpTransferAdapterInstance.deposit( originDomainID, recipientAddress, @@ -158,7 +157,7 @@ contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => const feeHandlerBalanceBefore = await web3.eth.getBalance(BasicFeeHandlerInstance.address); const depositorNativeBalanceBefore = await web3.eth.getBalance(depositorAddress); - await TruffleAssert.passes( + await Helpers.passes( GmpTransferAdapterInstance.deposit( originDomainID, recipientAddress, diff --git a/test/gmpTransferAdapter/fees/withdraw.js b/test/gmpTransferAdapter/fees/withdraw.js new file mode 100644 index 00000000..9835e89f --- /dev/null +++ b/test/gmpTransferAdapter/fees/withdraw.js @@ -0,0 +1,184 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const Ethers = require("ethers"); +const Helpers = require("../../helpers"); + +const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); +const GmpHandlerContract = artifacts.require( + "GmpHandler" +); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const XERC20FactoryContract = artifacts.require("XERC20Factory"); +const XERC20Contract = artifacts.require("XERC20"); +const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); + + +contract("Gmp transfer adapter - [Withdraw]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + + const adminAccount = accounts[0]; + const depositorAddress = accounts[1]; + const nonAdminAddress = accounts[2]; + const recipientAddress = accounts[3]; + + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; + const depositAmount = 10; + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = 10 + const mintingLimit = 500; + const burningLimit = 500; + const withdrawAmount = Ethers.utils.parseEther("1") + + const assertOnlyAdmin = (method) => { + return Helpers.expectToRevertWithCustomError( + method(), + "CallerNotAdmin()" + ); + }; + + let BridgeInstance; + let GmpTransferAdapterInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let depositFunctionSignature; + let GmpHandlerInstance; + let XERC20LockboxInstance; + let XERC20Instance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + accounts[0] + )) + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); + GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( + BridgeInstance.address, + GmpHandlerInstance.address, + resourceID, + ); + + XERC20FactoryInstance = await XERC20FactoryContract.new(); + const XERC20DeployResponse = await XERC20FactoryInstance.deployXERC20( + "sygmaETH", + "sETH", + [mintingLimit], + [burningLimit], + [GmpTransferAdapterInstance.address] + ); + // set XERC20 contract instance address to the address deployed via XERC20Factory + const deployedXERC20Address = XERC20DeployResponse.logs[0].args._xerc20 + XERC20Instance = await XERC20Contract.at(deployedXERC20Address) + const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( + XERC20Instance.address, + Ethers.constants.AddressZero, + true + ); + // set Lockbox contract instance address to the address deployed via XERC20Factory + const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox + XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); + + await XERC20LockboxInstance.depositNativeTo(depositorAddress, {value: transferredAmount}); + await XERC20Instance.increaseAllowance( + GmpTransferAdapterInstance.address, + depositAmount, + { + from: depositorAddress + } + ); + + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + await BasicFeeHandlerInstance.changeFee(originDomainID, resourceID, fee); + + depositFunctionSignature = Helpers.getFunctionSignature( + GmpTransferAdapterInstance, + "executeProposal" + ); + + const GmpHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + GmpHandlerInstance.address, + GmpHandlerSetResourceData + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: GmpTransferAdapterInstance.address, + value: "1000000000000000000" + }) + }); + + it("should fail if withdraw is called by non admin", async () => { + const adapterBalanceBefore = await web3.eth.getBalance(GmpTransferAdapterInstance.address); + + await assertOnlyAdmin(() => + GmpTransferAdapterInstance.withdraw( + recipientAddress, + withdrawAmount, + { + from: nonAdminAddress + } + ) + ); + + const adapterBalanceAfter = await web3.eth.getBalance(GmpTransferAdapterInstance.address); + assert.strictEqual( + Ethers.BigNumber.from(adapterBalanceBefore).toString(), + adapterBalanceAfter.toString() + ); + }); + + it("should successfully withdraw if called by admin", async () => { + const recipientBalanceBefore = await web3.eth.getBalance(recipientAddress); + const adapterBalanceBefore = await web3.eth.getBalance(GmpTransferAdapterInstance.address); + + await GmpTransferAdapterInstance.withdraw( + recipientAddress, + withdrawAmount, + { + from: adminAccount + } + ) + + const recipientBalanceAfter = await web3.eth.getBalance(recipientAddress); + const adapterBalanceAfter = await web3.eth.getBalance(GmpTransferAdapterInstance.address); + assert.strictEqual( + Ethers.BigNumber.from(recipientBalanceBefore).add(withdrawAmount).toString(), + Ethers.BigNumber.from(recipientBalanceAfter).toString() + ); + assert.strictEqual( + Ethers.BigNumber.from(adapterBalanceBefore).add(recipientBalanceBefore).toString(), + Ethers.BigNumber.from(adapterBalanceAfter).add(recipientBalanceAfter).toString() + ); + }); +}); diff --git a/test/gmpTransferAdapter/nativeTransfer/deposit.js b/test/gmpTransferAdapter/nativeTransfer/deposit.js index 2bb60970..96c781f5 100644 --- a/test/gmpTransferAdapter/nativeTransfer/deposit.js +++ b/test/gmpTransferAdapter/nativeTransfer/deposit.js @@ -16,7 +16,7 @@ const XERC20Contract = artifacts.require("XERC20"); const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); -contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => { +contract("Gmp transfer adapter - [Deposit XERC20 - wrapped native token]", async (accounts) => { const originDomainID = 1; const destinationDomainID = 2; const expectedDepositNonce = 1; @@ -24,6 +24,7 @@ contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => const depositorAddress = accounts[1]; const recipientAddress = accounts[3]; + const destinationMaxFee = 950000; const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; const depositAmount = 10; const fee = Ethers.utils.parseEther("0.1"); @@ -157,6 +158,20 @@ contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => }); it("depositEvent is emitted with expected values", async () => { + const preparedExecutionData = await GmpTransferAdapterInstance.prepareDepositData( + recipientAddress, + XERC20Instance.address, + depositAmount + ); + const depositData = Helpers.createGmpDepositData( + depositFunctionSignature, + GmpTransferAdapterInstance.address, + destinationMaxFee, + GmpTransferAdapterInstance.address, + preparedExecutionData, + false + ); + const depositTx = await GmpTransferAdapterInstance.deposit( originDomainID, recipientAddress, @@ -178,7 +193,9 @@ contract("Gmp transfer adapter - [Deposit - native token]", async (accounts) => event.destinationDomainID.toNumber() === originDomainID && event.resourceID === resourceID.toLowerCase() && event.depositNonce.toNumber() === expectedDepositNonce && - event.user === GmpTransferAdapterInstance.address + event.user === GmpTransferAdapterInstance.address && + event.data === depositData && + event.handlerResponse === null ); }); }); diff --git a/test/gmpTransferAdapter/nativeTransfer/executeProposal.js b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js index e9e8f253..e3463a21 100644 --- a/test/gmpTransferAdapter/nativeTransfer/executeProposal.js +++ b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js @@ -16,7 +16,7 @@ const XERC20FactoryContract = artifacts.require("XERC20Factory"); const XERC20Contract = artifacts.require("XERC20"); const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); -contract("Gmp transfer adapter - [Execute proposal - native token]", async (accounts) => { +contract("Gmp transfer adapter - [Execute proposal XERC20 - wrapped native token]", async (accounts) => { const originDomainID = 1; const destinationDomainID = 2; const adminAddress = accounts[0]; From da7ca5f7f4632ef4160ca16f4a3c76075d40ad99 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Thu, 8 Aug 2024 08:35:56 +0200 Subject: [PATCH 11/14] fix test --- test/gmpTransferAdapter/fees/withdraw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gmpTransferAdapter/fees/withdraw.js b/test/gmpTransferAdapter/fees/withdraw.js index 9835e89f..4e5ee906 100644 --- a/test/gmpTransferAdapter/fees/withdraw.js +++ b/test/gmpTransferAdapter/fees/withdraw.js @@ -142,7 +142,7 @@ contract("Gmp transfer adapter - [Withdraw]", async (accounts) => { const adapterBalanceBefore = await web3.eth.getBalance(GmpTransferAdapterInstance.address); await assertOnlyAdmin(() => - GmpTransferAdapterInstance.withdraw( + GmpTransferAdapterInstance.withdraw.call( recipientAddress, withdrawAmount, { From 0bf75c620a927701f2b68676ba5f195d90be24da Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Thu, 8 Aug 2024 21:21:17 +0200 Subject: [PATCH 12/14] add usage warning message to adapter contract --- contracts/adapters/GmpTransferAdapter.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol index 07625ecb..35df36fe 100644 --- a/contracts/adapters/GmpTransferAdapter.sol +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -9,6 +9,21 @@ import "../../contracts/interfaces/IFeeHandler.sol"; import "../XERC20/interfaces/IXERC20.sol"; import "./interfaces/IGmpTransferAdapter.sol"; +/** + .__ __. ______ .___________. __ ______ _______ + | \ | | / __ \ | || | / || ____| + | \| | | | | | `---| |----`| | | ,----'| |__ + | . ` | | | | | | | | | | | | __| + | |\ | | `--' | | | | | | `----.| |____ + |__| \__| \______/ |__| |__| \______||_______| + + Be careful when interacting with this contact as it enables + permissionless token addition and transfers via Sygma brige. + Always double check contract addresses and code you are interacting with + since a malicious actor could deploy a fake contract on a route + that is isn't set up by the Sygma team or a trusted 3rd party. + This can result in loss of all your funds. +*/ contract GmpTransferAdapter is IGmpTransferAdapter, AccessControl { using ERC165Checker for address; From 1fa44506bf32d8ec96278193c69b8ec4c47ba7e6 Mon Sep 17 00:00:00 2001 From: nmlinaric Date: Sun, 18 Aug 2024 13:16:51 +0200 Subject: [PATCH 13/14] remove excess contract logic --- contracts/adapters/GmpTransferAdapter.sol | 17 -- .../erc20Transfer/deposit.js | 7 - .../executeProposalDifferentAddresses.js | 7 - .../executeProposalSameAddresses.js | 7 - test/gmpTransferAdapter/fees/collectFee.js | 7 - test/gmpTransferAdapter/fees/withdraw.js | 184 ------------------ .../nativeTransfer/deposit.js | 7 - .../nativeTransfer/executeProposal.js | 7 - 8 files changed, 243 deletions(-) delete mode 100644 test/gmpTransferAdapter/fees/withdraw.js diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol index 35df36fe..81b35fab 100644 --- a/contracts/adapters/GmpTransferAdapter.sol +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -116,25 +116,10 @@ contract GmpTransferAdapter is IGmpTransferAdapter, AccessControl { IXERC20(XERC20Address).mint(recipient, amount); } - /** - @notice Used to manually transfer native tokens from Adapter. - @param recipient Address that should recieve the native tokens. - recipient address - */ - function withdraw(address recipient, uint256 amount) external onlyAdmin { - (bool success, ) = address(recipient).call{value: amount}(""); - if(!success) revert FailedFundsTransfer(); - emit Withdrawal(recipient, amount); - } - function setTokenPairAddress(address sourceTokenAddress, uint8 destinationDomainID, address destinationTokenAddress) external onlyAdmin { crossChainTokenPairs[sourceTokenAddress][destinationDomainID] = destinationTokenAddress; } - function slice(bytes calldata input, uint256 position) public pure returns (bytes memory) { - return input[position:]; - } - function prepareDepositData( address recipientAddress, address XERC20Address, @@ -142,6 +127,4 @@ contract GmpTransferAdapter is IGmpTransferAdapter, AccessControl { ) public view returns (bytes memory) { return abi.encode(recipientAddress, XERC20Address, bridgingAmount); } - - receive() external payable {} } diff --git a/test/gmpTransferAdapter/erc20Transfer/deposit.js b/test/gmpTransferAdapter/erc20Transfer/deposit.js index d1dd9c4f..a4c13b8b 100644 --- a/test/gmpTransferAdapter/erc20Transfer/deposit.js +++ b/test/gmpTransferAdapter/erc20Transfer/deposit.js @@ -142,13 +142,6 @@ contract("Gmp transfer adapter - [Deposit XERC20 - wrapped ERC20 token]", async // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) }); it("deposit can be made successfully and depositor tokens are burnt", async () => { diff --git a/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js b/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js index b136f328..d48688d3 100644 --- a/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js @@ -205,13 +205,6 @@ contract(`Gmp transfer adapter - // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) }); it("isProposalExecuted returns false if depositNonce is not used", async () => { diff --git a/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js b/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js index 58d8251e..7bb4fafa 100644 --- a/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js @@ -171,13 +171,6 @@ contract(`Gmp transfer adapter - // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) }); it("isProposalExecuted returns false if depositNonce is not used", async () => { diff --git a/test/gmpTransferAdapter/fees/collectFee.js b/test/gmpTransferAdapter/fees/collectFee.js index ed196148..7b188d3d 100644 --- a/test/gmpTransferAdapter/fees/collectFee.js +++ b/test/gmpTransferAdapter/fees/collectFee.js @@ -122,13 +122,6 @@ contract("Gmp transfer adapter - [Collect fee]", async (accounts) => { // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) }); it("should successfully charge fee on deposit", async () => { diff --git a/test/gmpTransferAdapter/fees/withdraw.js b/test/gmpTransferAdapter/fees/withdraw.js deleted file mode 100644 index 4e5ee906..00000000 --- a/test/gmpTransferAdapter/fees/withdraw.js +++ /dev/null @@ -1,184 +0,0 @@ -// The Licensed Work is (c) 2022 Sygma -// SPDX-License-Identifier: LGPL-3.0-only - -const Ethers = require("ethers"); -const Helpers = require("../../helpers"); - -const GmpTransferAdapterContract = artifacts.require("GmpTransferAdapter"); -const GmpHandlerContract = artifacts.require( - "GmpHandler" -); -const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); -const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); -const XERC20FactoryContract = artifacts.require("XERC20Factory"); -const XERC20Contract = artifacts.require("XERC20"); -const XERC20LockboxContract = artifacts.require("XERC20Lockbox"); - - -contract("Gmp transfer adapter - [Withdraw]", async (accounts) => { - const originDomainID = 1; - const destinationDomainID = 2; - - const adminAccount = accounts[0]; - const depositorAddress = accounts[1]; - const nonAdminAddress = accounts[2]; - const recipientAddress = accounts[3]; - - const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000500"; - const depositAmount = 10; - const fee = Ethers.utils.parseEther("0.1"); - const transferredAmount = 10 - const mintingLimit = 500; - const burningLimit = 500; - const withdrawAmount = Ethers.utils.parseEther("1") - - const assertOnlyAdmin = (method) => { - return Helpers.expectToRevertWithCustomError( - method(), - "CallerNotAdmin()" - ); - }; - - let BridgeInstance; - let GmpTransferAdapterInstance; - let BasicFeeHandlerInstance; - let FeeHandlerRouterInstance; - let depositFunctionSignature; - let GmpHandlerInstance; - let XERC20LockboxInstance; - let XERC20Instance; - - beforeEach(async () => { - await Promise.all([ - (BridgeInstance = await Helpers.deployBridge( - destinationDomainID, - accounts[0] - )) - ]); - - - FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( - BridgeInstance.address - ); - BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( - BridgeInstance.address, - FeeHandlerRouterInstance.address - ); - - GmpHandlerInstance = await GmpHandlerContract.new(BridgeInstance.address); - GmpTransferAdapterInstance = await GmpTransferAdapterContract.new( - BridgeInstance.address, - GmpHandlerInstance.address, - resourceID, - ); - - XERC20FactoryInstance = await XERC20FactoryContract.new(); - const XERC20DeployResponse = await XERC20FactoryInstance.deployXERC20( - "sygmaETH", - "sETH", - [mintingLimit], - [burningLimit], - [GmpTransferAdapterInstance.address] - ); - // set XERC20 contract instance address to the address deployed via XERC20Factory - const deployedXERC20Address = XERC20DeployResponse.logs[0].args._xerc20 - XERC20Instance = await XERC20Contract.at(deployedXERC20Address) - const lockboxDeployResponse = await XERC20FactoryInstance.deployLockbox( - XERC20Instance.address, - Ethers.constants.AddressZero, - true - ); - // set Lockbox contract instance address to the address deployed via XERC20Factory - const lockboxAddress = lockboxDeployResponse.logs[0].args._lockbox - XERC20LockboxInstance = await XERC20LockboxContract.at(lockboxAddress); - - await XERC20LockboxInstance.depositNativeTo(depositorAddress, {value: transferredAmount}); - await XERC20Instance.increaseAllowance( - GmpTransferAdapterInstance.address, - depositAmount, - { - from: depositorAddress - } - ); - - await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), - await FeeHandlerRouterInstance.adminSetResourceHandler( - originDomainID, - resourceID, - BasicFeeHandlerInstance.address - ), - await BasicFeeHandlerInstance.changeFee(originDomainID, resourceID, fee); - - depositFunctionSignature = Helpers.getFunctionSignature( - GmpTransferAdapterInstance, - "executeProposal" - ); - - const GmpHandlerSetResourceData = - Helpers.constructGenericHandlerSetResourceData( - depositFunctionSignature, - Helpers.blankFunctionDepositorOffset, - Helpers.blankFunctionSig - ); - await BridgeInstance.adminSetResource( - GmpHandlerInstance.address, - resourceID, - GmpHandlerInstance.address, - GmpHandlerSetResourceData - ); - - // set MPC address to unpause the Bridge - await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) - }); - - it("should fail if withdraw is called by non admin", async () => { - const adapterBalanceBefore = await web3.eth.getBalance(GmpTransferAdapterInstance.address); - - await assertOnlyAdmin(() => - GmpTransferAdapterInstance.withdraw.call( - recipientAddress, - withdrawAmount, - { - from: nonAdminAddress - } - ) - ); - - const adapterBalanceAfter = await web3.eth.getBalance(GmpTransferAdapterInstance.address); - assert.strictEqual( - Ethers.BigNumber.from(adapterBalanceBefore).toString(), - adapterBalanceAfter.toString() - ); - }); - - it("should successfully withdraw if called by admin", async () => { - const recipientBalanceBefore = await web3.eth.getBalance(recipientAddress); - const adapterBalanceBefore = await web3.eth.getBalance(GmpTransferAdapterInstance.address); - - await GmpTransferAdapterInstance.withdraw( - recipientAddress, - withdrawAmount, - { - from: adminAccount - } - ) - - const recipientBalanceAfter = await web3.eth.getBalance(recipientAddress); - const adapterBalanceAfter = await web3.eth.getBalance(GmpTransferAdapterInstance.address); - assert.strictEqual( - Ethers.BigNumber.from(recipientBalanceBefore).add(withdrawAmount).toString(), - Ethers.BigNumber.from(recipientBalanceAfter).toString() - ); - assert.strictEqual( - Ethers.BigNumber.from(adapterBalanceBefore).add(recipientBalanceBefore).toString(), - Ethers.BigNumber.from(adapterBalanceAfter).add(recipientBalanceAfter).toString() - ); - }); -}); diff --git a/test/gmpTransferAdapter/nativeTransfer/deposit.js b/test/gmpTransferAdapter/nativeTransfer/deposit.js index 96c781f5..5cd681e0 100644 --- a/test/gmpTransferAdapter/nativeTransfer/deposit.js +++ b/test/gmpTransferAdapter/nativeTransfer/deposit.js @@ -124,13 +124,6 @@ contract("Gmp transfer adapter - [Deposit XERC20 - wrapped native token]", async // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) }); it("deposit can be made successfully and depositor native tokens are deducted", async () => { diff --git a/test/gmpTransferAdapter/nativeTransfer/executeProposal.js b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js index e3463a21..3b14b1b0 100644 --- a/test/gmpTransferAdapter/nativeTransfer/executeProposal.js +++ b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js @@ -157,13 +157,6 @@ contract("Gmp transfer adapter - [Execute proposal XERC20 - wrapped native token // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - - // send ETH to destination adapter for transfers - await web3.eth.sendTransaction({ - from: depositorAddress, - to: GmpTransferAdapterInstance.address, - value: "1000000000000000000" - }) }); it("isProposalExecuted returns false if depositNonce is not used", async () => { From b3e885694b5143fa27fa28ee2f9847e54228b68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Mlinari=C4=87?= <35880252+nmlinaric@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:18:01 +0200 Subject: [PATCH 14/14] improve disclaimer message Co-authored-by: Oleksii Matiiasevych --- contracts/adapters/GmpTransferAdapter.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/adapters/GmpTransferAdapter.sol b/contracts/adapters/GmpTransferAdapter.sol index 81b35fab..6dfbc228 100644 --- a/contracts/adapters/GmpTransferAdapter.sol +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -18,11 +18,11 @@ import "./interfaces/IGmpTransferAdapter.sol"; |__| \__| \______/ |__| |__| \______||_______| Be careful when interacting with this contact as it enables - permissionless token addition and transfers via Sygma brige. - Always double check contract addresses and code you are interacting with - since a malicious actor could deploy a fake contract on a route - that is isn't set up by the Sygma team or a trusted 3rd party. - This can result in loss of all your funds. + permissionless token addition and transfers via Sygma bridge. + Make sure that a malicious actor cannot deploy arbitrary code + to the same address as your XERC20 token on another chain. + This would allow them to burn fake tokens on chain A then mint + legit tokens on chain B. */ contract GmpTransferAdapter is IGmpTransferAdapter, AccessControl { using ERC165Checker for address;