diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b77fded7..7bdcf273 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '14.x' + node-version: '16.x' - run: yarn install --frozen-lockfile --non-interactive @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '14.x' + node-version: '16.x' - run: yarn install --frozen-lockfile --non-interactive - name: Run linter diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 990ac0cd..a93b7458 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - name: Checkout code uses: actions/checkout@v2.4.0 @@ -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 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - name: Checkout code uses: actions/checkout@v2.4.0 @@ -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 }} 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/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..0c6358fb 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 " @@ -19,7 +19,11 @@ 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 + +test-forked: + @echo " > \033[32mTesting contracts... \033[0m " + truffle test --stacktrace testUnderForked/* start-geth: @echo " > \033[32mStarting geth... \033[0m " 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..6dfbc228 --- /dev/null +++ b/contracts/adapters/GmpTransferAdapter.sol @@ -0,0 +1,130 @@ +// 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"; +import "./interfaces/IGmpTransferAdapter.sol"; + +/** + .__ __. ______ .___________. __ ______ _______ + | \ | | / __ \ | || | / || ____| + | \| | | | | | `---| |----`| | | ,----'| |__ + | . ` | | | | | | | | | | | | __| + | |\ | | `--' | | | | | | `----.| |____ + |__| \__| \______/ |__| |__| \______||_______| + + Be careful when interacting with this contact as it enables + 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; + + 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. + 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); + } + + modifier onlyAdmin() { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert CallerNotAdmin(); + _; + } + + 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(); + } + + 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 + IGmpTransferAdapter(address(this)).executeProposal.selector, + // uint8 len(executeContractAddress) + uint8(20), + // bytes executeContractAddress + address(this), + // uint8 len(executionDataDepositor) + uint8(20), + // bytes executionDataDepositor + address(this), + // bytes executionDataDepositor + executionData + prepareDepositData(recipientAddress, destinationToken, 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 setTokenPairAddress(address sourceTokenAddress, uint8 destinationDomainID, address destinationTokenAddress) external onlyAdmin { + crossChainTokenPairs[sourceTokenAddress][destinationDomainID] = destinationTokenAddress; + } + + function prepareDepositData( + address recipientAddress, + address XERC20Address, + uint256 bridgingAmount + ) public view returns (bytes memory) { + return abi.encode(recipientAddress, XERC20Address, bridgingAmount); + } +} 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; +} diff --git a/package.json b/package.json index 92bbb010..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 ." @@ -61,7 +61,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", @@ -83,6 +83,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/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 bce7a9cc..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 -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 -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 -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/adapters/native/deposit.js b/test/adapters/native/deposit.js index 76bc8f32..8fb1b674 100644 --- a/test/adapters/native/deposit.js +++ b/test/adapters/native/deposit.js @@ -133,7 +133,7 @@ contract("Bridge - [deposit - native token]", async (accounts) => { }); it("Should revert if destination domain is current bridge domain", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( NativeTokenAdapterInstance.deposit(originDomainID, btcRecipientAddress, { from: depositorAddress, value: depositAmount @@ -155,7 +155,7 @@ contract("Bridge - [deposit - native token]", async (accounts) => { emptySetResourceData ); - await TruffleAssert.reverts( + await Helpers.reverts( NativeTokenAdapterInstance.deposit(destinationDomainID, btcRecipientAddress, { from: depositorAddress, value: depositAmount 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 41b9d0c1..55953a65 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) => { @@ -15,27 +16,25 @@ contract("FROSTKeygen", (accounts) => { const tx = await FROSTKeygenInstance.startFROSTKeygen({from: accounts[0]}) TruffleAssert.eventEmitted(tx, "StartedFROSTKeygen"); - }); it("should revert when startFROSTKeygen is not called by the owner", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( FROSTKeygenInstance.startFROSTKeygen({from: accounts[1]}), ) - }); it("should revert when keygen ended", async() => { const tx = await FROSTKeygenInstance.endFROSTKeygen({from: accounts[0]}) TruffleAssert.eventEmitted(tx, "EndedFROSTKeygen"); - await TruffleAssert.reverts( - FROSTKeygenInstance.startFROSTKeygen({from: accounts[1]}), + await Helpers.reverts( + FROSTKeygenInstance.startFROSTKeygen({from: accounts[0]}) ) }); it("should revert when end keygen not called by owner", async() => { - await TruffleAssert.reverts( + await Helpers.reverts( FROSTKeygenInstance.endFROSTKeygen({from: accounts[1]}), ) }); 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/gmpTransferAdapter/erc20Transfer/deposit.js b/test/gmpTransferAdapter/erc20Transfer/deposit.js new file mode 100644 index 00000000..a4c13b8b --- /dev/null +++ b/test/gmpTransferAdapter/erc20Transfer/deposit.js @@ -0,0 +1,211 @@ +// 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 XERC20 - wrapped ERC20 token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const expectedDepositNonce = 1; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[3]; + + const destinationMaxFee = 950000; + 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); + }); + + 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 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, + 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 && + 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..d48688d3 --- /dev/null +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposalDifferentAddresses.js @@ -0,0 +1,406 @@ +// 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); + }); + + 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/executeProposalSameAddresses.js b/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js new file mode 100644 index 00000000..7bb4fafa --- /dev/null +++ b/test/gmpTransferAdapter/erc20Transfer/executeProposalSameAddresses.js @@ -0,0 +1,366 @@ +// 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 same 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 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); + }); + + 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/fees/collectFee.js b/test/gmpTransferAdapter/fees/collectFee.js new file mode 100644 index 00000000..7b188d3d --- /dev/null +++ b/test/gmpTransferAdapter/fees/collectFee.js @@ -0,0 +1,178 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const Helpers = require("../../helpers"); +const Ethers = require("ethers"); + +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 - [Collect fee]", 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); + }); + + it("should successfully charge fee on deposit", async () => { + const feeHandlerBalanceBefore = await web3.eth.getBalance(BasicFeeHandlerInstance.address); + + await Helpers.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 Helpers.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/nativeTransfer/deposit.js b/test/gmpTransferAdapter/nativeTransfer/deposit.js new file mode 100644 index 00000000..5cd681e0 --- /dev/null +++ b/test/gmpTransferAdapter/nativeTransfer/deposit.js @@ -0,0 +1,195 @@ +// 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 XERC20 - wrapped native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const expectedDepositNonce = 1; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[3]; + + const destinationMaxFee = 950000; + 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); + }); + + 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 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, + 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 && + event.data === depositData && + event.handlerResponse === null + ); + }); + }); +}); diff --git a/test/gmpTransferAdapter/nativeTransfer/executeProposal.js b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js new file mode 100644 index 00000000..3b14b1b0 --- /dev/null +++ b/test/gmpTransferAdapter/nativeTransfer/executeProposal.js @@ -0,0 +1,352 @@ +// 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 XERC20 - wrapped 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); + }); + + 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/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..b91dc0ad 100644 --- a/test/handlers/erc721/depositBurn.js +++ b/test/handlers/erc721/depositBurn.js @@ -1,8 +1,6 @@ // The Licensed Work is (c) 2022 Sygma // SPDX-License-Identifier: LGPL-3.0-only -const TruffleAssert = require("truffle-assertions"); - const Helpers = require("../../helpers"); const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); @@ -69,13 +67,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 +125,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 a55a9720..9d4a373d 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -368,13 +368,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); @@ -390,7 +384,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 = { @@ -424,5 +439,7 @@ module.exports = { mockSignTypedProposalWithInvalidChainID, createDepositProposalDataFromHandlerResponse, createGmpExecutionData, - expectToRevertWithCustomError + expectToRevertWithCustomError, + reverts, + passes, }; diff --git a/test/retry/retry.js b/test/retry/retry.js index 2ffdaf6b..6acded8e 100644 --- a/test/retry/retry.js +++ b/test/retry/retry.js @@ -1,6 +1,8 @@ // The Licensed Work is (c) 2022 Sygma // SPDX-License-Identifier: LGPL-3.0-only +const Helpers = require("../helpers"); + const TruffleAssert = require("truffle-assertions"); const Retry = artifacts.require("Retry") @@ -18,16 +20,16 @@ contract("Retry", (accounts) => { it("should emit Retry event when retry is called by the owner", async () => { const tx = await RetryInstance.retry( - sourceDomainID, - destinationDomainID, - blockHeight, - resourceID, + sourceDomainID, + destinationDomainID, + blockHeight, + resourceID, {from: accounts[0]}) TruffleAssert.eventEmitted(tx, "Retry", (event) => { return ( - event.sourceDomainID.toNumber() === sourceDomainID && - event.destinationDomainID.toNumber() === destinationDomainID && + event.sourceDomainID.toNumber() === sourceDomainID && + event.destinationDomainID.toNumber() === destinationDomainID && event.blockHeight.toNumber() === blockHeight && event.resourceID === resourceID ); @@ -35,9 +37,9 @@ contract("Retry", (accounts) => { }); it("should revert when retry is not called by the owner", async () => { - await TruffleAssert.reverts( + await Helpers.reverts( RetryInstance.retry(sourceDomainID, destinationDomainID, blockHeight, resourceID, {from: accounts[1]}), - "Ownable: caller is not the owner." + "Ownable: caller is not the owner" ) }); }) 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/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 ) 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 2b3e485a..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== @@ -10088,13 +10123,10 @@ solidity-coverage@^0.7.20: shelljs "^0.8.3" web3-utils "^1.3.0" -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" +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.17, source-map-support@^0.5.3: version "0.5.21" @@ -11010,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" @@ -11575,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" @@ -11688,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== @@ -11720,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"