diff --git a/README.md b/README.md index 34d61248..a995837f 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,6 @@ to ensure a statement and patch release is made in a timely manner. Please email us a description of the flaw and any related information (e.g. reproduction steps, version) to [dev@buildwithsygma.com](mailto:dev@buildwithsygma.com). + +## Audits +You can find audit reports inside [`/audits`](./audits/) \ No newline at end of file diff --git a/audits/veridise-24-08-2024.pdf b/audits/veridise-24-08-2024.pdf new file mode 100644 index 00000000..ab98d739 Binary files /dev/null and b/audits/veridise-24-08-2024.pdf differ diff --git a/contracts/adapters/nativeTokens/NativeTokenAdapter.sol b/contracts/adapters/nativeTokens/NativeTokenAdapter.sol index 22a0b59f..ed6a7b2b 100644 --- a/contracts/adapters/nativeTokens/NativeTokenAdapter.sol +++ b/contracts/adapters/nativeTokens/NativeTokenAdapter.sol @@ -1,44 +1,76 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.11; -import "@openzeppelin/contracts/access/AccessControl.sol"; import "../../interfaces/IBridge.sol"; import "../../interfaces/IFeeHandler.sol"; -contract NativeTokenAdapter is AccessControl { +contract NativeTokenAdapter { IBridge public immutable _bridge; bytes32 public immutable _resourceID; - event Withdrawal(address recipient, uint amount); - error SenderNotAdmin(); error InsufficientMsgValueAmount(uint256 amount); error MsgValueLowerThanFee(uint256 amount); - error TokenWithdrawalFailed(); error InsufficientBalance(); error FailedFundsTransfer(); - - modifier onlyAdmin() { - if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert SenderNotAdmin(); - _; - } + error ZeroGas(); constructor(address bridge, bytes32 resourceID) { _bridge = IBridge(bridge); _resourceID = resourceID; - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } function deposit(uint8 destinationDomainID, string calldata recipientAddress) external payable { - if (msg.value <= 0) revert InsufficientMsgValueAmount(msg.value); + bytes memory depositData = abi.encodePacked( + uint256(bytes(recipientAddress).length), + recipientAddress + ); + depositGeneral(destinationDomainID, depositData); + } + + function depositToEVM(uint8 destinationDomainID, address recipient) external payable { + bytes memory depositData = abi.encodePacked( + uint256(20), + recipient + ); + depositGeneral(destinationDomainID, depositData); + } + + /** + @notice Makes a native token deposit with an included message. + @param destinationDomainID ID of destination chain. + @param recipient The destination chain contract address that implements the ISygmaMessageReceiver interface. + If the recipient is set to zero address then it will be replaced on the destination with + the address of the DefaultMessageReceiver which is a generic ISygmaMessageReceiver implementation. + @param gas The amount of gas needed to successfully execute the call to recipient on the destination. Fee amount is + directly affected by this value. + @param message Arbitrary encoded bytes array that will be passed as the third argument in the + ISygmaMessageReceiver(recipient).handleSygmaMessage(_, _, message) call. If you intend to use the + DefaultMessageReceiver, make sure to encode the message to comply with the + DefaultMessageReceiver.handleSygmaMessage() message decoding implementation. + */ + function depositToEVMWithMessage(uint8 destinationDomainID, address recipient, uint256 gas, bytes calldata message) external payable { + if (gas == 0) revert ZeroGas(); + bytes memory depositData = abi.encodePacked( + uint256(20), + recipient, + gas, + uint256(message.length), + message + ); + depositGeneral(destinationDomainID, depositData); + } + + function depositGeneral(uint8 destinationDomainID, bytes memory depositDataAfterAmount) public payable { + if (msg.value == 0) revert InsufficientMsgValueAmount(msg.value); address feeHandlerRouter = _bridge._feeHandler(); (uint256 fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( address(this), _bridge._domainID(), destinationDomainID, _resourceID, - "", // depositData - not parsed + abi.encodePacked(msg.value, depositDataAfterAmount), "" // feeData - not parsed ); @@ -47,16 +79,21 @@ contract NativeTokenAdapter is AccessControl { bytes memory depositData = abi.encodePacked( transferAmount, - bytes(recipientAddress).length, - recipientAddress + depositDataAfterAmount ); _bridge.deposit{value: fee}(destinationDomainID, _resourceID, depositData, ""); address nativeHandlerAddress = _bridge._resourceIDToHandlerAddress(_resourceID); (bool success, ) = nativeHandlerAddress.call{value: transferAmount}(""); - if(!success) revert FailedFundsTransfer(); + if (!success) revert FailedFundsTransfer(); + uint256 leftover = address(this).balance; + if (leftover > 0) { + (success, ) = payable(msg.sender).call{value: leftover}(""); + // Do not revert if sender does not want to receive. + } } + // For an unlikely case when part of the fee is returned. receive() external payable {} } diff --git a/contracts/handlers/DefaultMessageReceiver.sol b/contracts/handlers/DefaultMessageReceiver.sol new file mode 100644 index 00000000..70537d01 --- /dev/null +++ b/contracts/handlers/DefaultMessageReceiver.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.11; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "../utils/AccessControl.sol"; +import "../interfaces/ISygmaMessageReceiver.sol"; + +contract DefaultMessageReceiver is ISygmaMessageReceiver, AccessControl, ERC721Holder, ERC1155Holder { + bytes32 public constant SYGMA_HANDLER_ROLE = keccak256("SYGMA_HANDLER_ROLE"); + + address internal constant zeroAddress = address(0); + + uint256 public immutable _recoverGas; + + struct Action { + uint256 nativeValue; + address callTo; + address approveTo; + address tokenSend; + address tokenReceive; + bytes data; + } + + error InsufficientGasLimit(); + error InvalidContract(); + error InsufficientPermission(); + error ActionFailed(); + error InsufficientNativeBalance(); + error ReturnNativeLeftOverFailed(); + + event Executed( + bytes32 transactionId, + address tokenSend, + address receiver, + uint256 amount + ); + + event TransferRecovered( + bytes32 transactionId, + address tokenSend, + address receiver, + uint256 amount + ); + + /// Constructor /// + + /// @param sygmaHandlers The contract addresses with access to message processing. + /// @param recoverGas The amount of gas needed to forward the original amount to receiver. + constructor(address[] memory sygmaHandlers, uint256 recoverGas) { + _recoverGas = recoverGas; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + for (uint i = 0; i < sygmaHandlers.length; i++) { + _setupRole(SYGMA_HANDLER_ROLE, sygmaHandlers[i]); + } + } + + /** + @notice Users have to understand the design and limitations behind the Actions processing. + The contract will try to return all the leftover tokens and native token to the + receiver address. This logic is applied to the native token if there was a balance + increase during the message processing, then to the tokenSent which is received from + Sygma proposal and finally to every Action.tokenReceive. In the vast majority of + cases that would be enough, though user can come up with a scenario where an Action + produces results in a receival of more than one token, while only one could be + specified in this particular Action.tokenReceive property. In such a case it is + a users responsibility to either send it all with a transferBalanceAction() Action or to + include an extra action[s] with tokenReceive set to each of the tokens received. + */ + function handleSygmaMessage( + address tokenSent, + uint256 amount, + bytes memory message + ) external payable override { + if (!hasRole(SYGMA_HANDLER_ROLE, _msgSender())) revert InsufficientPermission(); + ( + bytes32 transactionId, + Action[] memory actions, + address receiver + ) = abi.decode(message, (bytes32, Action[], address)); + + _execute(transactionId, actions, tokenSent, payable(receiver), amount); + } + + function _execute( + bytes32 transactionId, + Action[] memory actions, + address tokenSent, + address payable receiver, + uint256 amount + ) internal { + uint256 cacheGasLeft = gasleft(); + if (cacheGasLeft < _recoverGas) revert InsufficientGasLimit(); + + uint256 startingNativeBalance = address(this).balance - msg.value; + /// We are wrapping the Actions processing in new call in order to be + /// able to recover, ie. send funds to the receiver, in case of fail or + /// running out of gas. Otherwise we can only revert whole execution + /// which would result in the need to manually process proposal resolution + /// to unstuck the assets. + try this.performActions{gas: cacheGasLeft - _recoverGas}( + tokenSent, + receiver, + startingNativeBalance, + actions + ) { + emit Executed( + transactionId, + tokenSent, + receiver, + amount + ); + } catch { + cacheGasLeft = gasleft(); + if (cacheGasLeft < _recoverGas) revert InsufficientGasLimit(); + transferBalance(tokenSent, receiver); + if (address(this).balance > startingNativeBalance) { + transferNativeBalance(receiver); + } + + emit TransferRecovered( + transactionId, + tokenSent, + receiver, + amount + ); + } + } + + /// @dev See the comment inside of the _execute() function. + function performActions( + address tokenSent, + address payable receiver, + uint256 startingNativeBalance, + Action[] memory actions + ) external { + if (msg.sender != address(this)) revert InsufficientPermission(); + + uint256 numActions = actions.length; + for (uint256 i = 0; i < numActions; i++) { + // Allow EOA if the data is empty. Could be used to send native currency. + if (!isContract(actions[i].callTo) && actions[i].data.length > 0) revert InvalidContract(); + uint256 nativeValue = actions[i].nativeValue; + if (nativeValue > 0 && address(this).balance < nativeValue) { + revert InsufficientNativeBalance(); + } + approveERC20(IERC20(actions[i].tokenSend), actions[i].approveTo, type(uint256).max); + + (bool success, ) = actions[i].callTo.call{value: nativeValue}(actions[i].data); + if (!success) { + revert ActionFailed(); + } + } + if (address(this).balance > startingNativeBalance) { + transferNativeBalance(receiver); + } + transferBalance(tokenSent, receiver); + returnLeftOvers(actions, receiver); + } + + function returnLeftOvers(Action[] memory actions, address payable receiver) internal { + for (uint256 i; i < actions.length; i++) { + transferBalance(actions[i].tokenReceive, receiver); + approveERC20(IERC20(actions[i].tokenSend), actions[i].approveTo, 0); + } + } + + function transferNativeBalance(address payable receiver) internal { + (bool success, ) = receiver.call{value: address(this).balance}(""); + if (!success) { + revert ReturnNativeLeftOverFailed(); + } + } + + function transferBalance(address token, address receiver) internal { + if (token != zeroAddress) { + uint256 tokenBalance = IERC20(token).balanceOf(address(this)); + if (tokenBalance > 0) { + SafeERC20.safeTransfer(IERC20(token), receiver, tokenBalance); + } + } + } + + /// @notice Helper function that could be used as an Action to itself to transfer whole + /// @notice balance of a particular token. + function transferBalanceAction(address token, address receiver) external { + if (msg.sender != address(this)) revert InsufficientPermission(); + if (token != zeroAddress) { + transferBalance(token, receiver); + } else { + transferNativeBalance(payable(receiver)); + } + } + + function isContract(address contractAddr) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(contractAddr) + } + return size > 0; + } + + function approveERC20( + IERC20 token, + address spender, + uint256 amount + ) internal { + if (address(token) != zeroAddress && spender != zeroAddress) { + // Ad-hoc SafeERC20.forceApprove() because OZ lib from dependencies does not have one yet. + (bool success, ) = address(token).call(abi.encodeWithSelector(token.approve.selector, spender, 0)); + if (amount > 0) { + (success, ) = address(token).call(abi.encodeWithSelector(token.approve.selector, spender, amount)); + } + } + } + + receive() external payable {} +} diff --git a/contracts/handlers/DepositDataHelper.sol b/contracts/handlers/DepositDataHelper.sol new file mode 100644 index 00000000..5f84ad28 --- /dev/null +++ b/contracts/handlers/DepositDataHelper.sol @@ -0,0 +1,106 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +import "./ERCHandlerHelpers.sol"; +import "../interfaces/ISygmaMessageReceiver.sol"; + +/** + @title Contract to simplify parsing of deposit data with an optional message. + @author ChainSafe Systems. + */ +contract DepositDataHelper is ERCHandlerHelpers { + using SanityChecks for *; + + address public immutable _defaultMessageReceiver; + uint16 internal constant maxReturnBytes = 256; + address internal constant transformRecipient = address(0); + + enum OptionalMessageCheck { Absent, Valid, Invalid } + + struct DepositData { + address tokenAddress; + uint256 amount; + address recipientAddress; + uint256 externalAmount; + uint256 gas; + OptionalMessageCheck optionalMessageCheck; + bytes message; + } + + /** + @param bridgeAddress Contract address of previously deployed Bridge. + @param defaultMessageReceiver Contract address of previously deployed DefaultMessageReceiver. + */ + constructor( + address bridgeAddress, + address defaultMessageReceiver + ) ERCHandlerHelpers(bridgeAddress) { + _defaultMessageReceiver = defaultMessageReceiver; + } + + /** + @param resourceID ResourceID to be used when making deposits. + @param data Consists of {amount}, {recipient}, {optionalGas}, {optionalMessage}, + padded to 32 bytes. + @notice Data passed into the function should be constructed as follows: + amount uint256 bytes 0 - 32 + destinationRecipientAddress length uint256 bytes 32 - 64 + destinationRecipientAddress bytes bytes 64 - (64 + len(destinationRecipientAddress)) + optionalGas uint256 bytes (64 + len(destinationRecipientAddress)) - (96 + len(destinationRecipientAddress)) + optionalMessage length uint256 bytes (96 + len(destinationRecipientAddress)) - (128 + len(destinationRecipientAddress)) + optionalMessage bytes bytes (160 + len(destinationRecipientAddress)) - END + */ + function parseDepositData(bytes32 resourceID, bytes calldata data) internal view returns(DepositData memory) { + uint256 amount; + uint256 lenDestinationRecipientAddress; + + (amount, lenDestinationRecipientAddress) = abi.decode(data, (uint256, uint256)); + lenDestinationRecipientAddress.mustBe(20); + address recipientAddress = address(bytes20(bytes(data[64:84]))); + + address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; + uint256 externalAmount = convertToExternalBalance(tokenAddress, amount); + + // Optional message recipient transformation. + uint256 pointer = 84; + uint256 gas; + uint256 messageLength; + bytes memory message; + OptionalMessageCheck optionalMessageCheck; + if (data.length > (pointer + 64)) { + (gas, messageLength) = abi.decode(data[pointer:], (uint256, uint256)); + pointer += 64; + if (gas > 0 && messageLength > 0 && (messageLength + pointer) <= data.length) { + optionalMessageCheck = OptionalMessageCheck.Valid; + if (recipientAddress == transformRecipient) { + recipientAddress = _defaultMessageReceiver; + } + message = abi.encodeWithSelector( + ISygmaMessageReceiver(recipientAddress).handleSygmaMessage.selector, + tokenAddress, + externalAmount, + bytes(data[pointer:pointer + messageLength]) + ); + } else { + optionalMessageCheck = OptionalMessageCheck.Invalid; + message = abi.encode( + tokenAddress, + recipientAddress, + amount, + abi.encodeWithSignature("InvalidEncoding()") + ); + } + } + + return DepositData( + tokenAddress, + amount, + recipientAddress, + externalAmount, + gas, + optionalMessageCheck, + message + ); + } +} diff --git a/contracts/handlers/ERC1155Handler.sol b/contracts/handlers/ERC1155Handler.sol index 07c661d0..b11a531f 100644 --- a/contracts/handlers/ERC1155Handler.sol +++ b/contracts/handlers/ERC1155Handler.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; contract ERC1155Handler is IHandler, ERCHandlerHelpers, ERC1155Safe, ERC1155Holder { + using SanityChecks for *; using ERC165Checker for address; bytes private constant EMPTY_BYTES = ""; @@ -70,6 +71,7 @@ contract ERC1155Handler is IHandler, ERCHandlerHelpers, ERC1155Safe, ERC1155Hold (tokenIDs, amounts, recipient, transferData) = abi.decode(data, (uint[], uint[], bytes, bytes)); + recipient.length.mustBe(20); bytes20 recipientAddress; assembly { @@ -101,6 +103,7 @@ contract ERC1155Handler is IHandler, ERCHandlerHelpers, ERC1155Safe, ERC1155Hold (tokenAddress, recipient, tokenIDs, amounts, transferData) = abi.decode(data, (address, address, uint[], uint[], bytes)); + recipient.mustNotBeZero(); releaseBatchERC1155(tokenAddress, address(this), recipient, tokenIDs, amounts, transferData); } diff --git a/contracts/handlers/ERC20Handler.sol b/contracts/handlers/ERC20Handler.sol index eb4aa169..d9ffc350 100644 --- a/contracts/handlers/ERC20Handler.sol +++ b/contracts/handlers/ERC20Handler.sol @@ -4,42 +4,53 @@ pragma solidity 0.8.11; import "../interfaces/IHandler.sol"; import "./ERCHandlerHelpers.sol"; +import "./DepositDataHelper.sol"; import "../ERC20Safe.sol"; +import "../utils/ExcessivelySafeCall.sol"; /** @title Handles ERC20 deposits and deposit executions. @author ChainSafe Systems. @notice This contract is intended to be used with the Bridge contract. */ -contract ERC20Handler is IHandler, ERCHandlerHelpers, ERC20Safe { +contract ERC20Handler is IHandler, ERCHandlerHelpers, DepositDataHelper, ERC20Safe { + using SanityChecks for *; + using ExcessivelySafeCall for address; + + error OptionalMessageCallFailed(); + /** @param bridgeAddress Contract address of previously deployed Bridge. */ constructor( - address bridgeAddress - ) ERCHandlerHelpers(bridgeAddress) { - } + address bridgeAddress, + address defaultMessageReceiver + ) DepositDataHelper(bridgeAddress, defaultMessageReceiver) {} /** @notice A deposit is initiated by making a deposit in the Bridge contract. @param resourceID ResourceID used to find address of token to be used for deposit. @param depositor Address of account making the deposit in the Bridge contract. - @param data Consists of {amount} padded to 32 bytes. + @param data Consists of {amount}, {recipient}, {optionalGas}, {optionalMessage}, + padded to 32 bytes. @notice Data passed into the function should be constructed as follows: - amount uint256 bytes 0 - 32 - destinationRecipientAddress length uint256 bytes 32 - 64 - destinationRecipientAddress bytes bytes 64 - END + amount uint256 bytes 0 - 32 + destinationRecipientAddress length uint256 bytes 32 - 64 + destinationRecipientAddress bytes bytes 64 - (64 + len(destinationRecipientAddress)) + optionalGas uint256 bytes (64 + len(destinationRecipientAddress)) - (96 + len(destinationRecipientAddress)) + optionalMessage length uint256 bytes (96 + len(destinationRecipientAddress)) - (128 + len(destinationRecipientAddress)) + optionalMessage bytes bytes (160 + len(destinationRecipientAddress)) - END @dev Depending if the corresponding {tokenAddress} for the parsed {resourceID} is marked true in {_tokenContractAddressToTokenProperties[tokenAddress].isBurnable}, deposited tokens will be burned, if not, they will be locked. - @return an empty data. + @return 32-length byte array with internal bridge amount OR empty byte array if conversion is not needed. */ function deposit( bytes32 resourceID, address depositor, bytes calldata data ) external override onlyBridge returns (bytes memory) { - uint256 amount; - (amount) = abi.decode(data, (uint)); + uint256 amount; + (amount) = abi.decode(data, (uint256)); address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; if (!_tokenContractAddressToTokenProperties[tokenAddress].isWhitelisted) revert ContractAddressNotWhitelisted(tokenAddress); @@ -50,43 +61,46 @@ contract ERC20Handler is IHandler, ERCHandlerHelpers, ERC20Safe { lockERC20(tokenAddress, depositor, address(this), amount); } - return abi.encodePacked(convertToInternalBalance(tokenAddress, amount)); + return convertToInternalBalance(tokenAddress, amount); } /** @notice Proposal execution should be initiated when a proposal is finalized in the Bridge contract. by a relayer on the deposit's destination chain. @param resourceID ResourceID to be used when making deposits. - @param data Consists of {resourceID}, {amount}, {lenDestinationRecipientAddress}, - and {destinationRecipientAddress} all padded to 32 bytes. + @param data Consists of {amount}, {recipient}, {optionalGas}, {optionalMessage}, + padded to 32 bytes. @notice Data passed into the function should be constructed as follows: - amount uint256 bytes 0 - 32 - destinationRecipientAddress length uint256 bytes 32 - 64 - destinationRecipientAddress bytes bytes 64 - END + amount uint256 bytes 0 - 32 + destinationRecipientAddress length uint256 bytes 32 - 64 + destinationRecipientAddress bytes bytes 64 - (64 + len(destinationRecipientAddress)) + optionalGas uint256 bytes (64 + len(destinationRecipientAddress)) - (96 + len(destinationRecipientAddress)) + optionalMessage length uint256 bytes (96 + len(destinationRecipientAddress)) - (128 + len(destinationRecipientAddress)) + optionalMessage bytes bytes (160 + len(destinationRecipientAddress)) - END */ function executeProposal(bytes32 resourceID, bytes calldata data) external override onlyBridge returns (bytes memory) { - uint256 amount; - uint256 lenDestinationRecipientAddress; - bytes memory destinationRecipientAddress; + DepositData memory depositData = parseDepositData(resourceID, data); - (amount, lenDestinationRecipientAddress) = abi.decode(data, (uint, uint)); - destinationRecipientAddress = bytes(data[64:64 + lenDestinationRecipientAddress]); + if (!_tokenContractAddressToTokenProperties[depositData.tokenAddress].isWhitelisted) revert ContractAddressNotWhitelisted(depositData.tokenAddress); - bytes20 recipientAddress; - address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; - - assembly { - recipientAddress := mload(add(destinationRecipientAddress, 0x20)) + if (_tokenContractAddressToTokenProperties[depositData.tokenAddress].isBurnable) { + mintERC20(depositData.tokenAddress, depositData.recipientAddress, depositData.externalAmount); + } else { + releaseERC20(depositData.tokenAddress, depositData.recipientAddress, depositData.externalAmount); } - if (!_tokenContractAddressToTokenProperties[tokenAddress].isWhitelisted) revert ContractAddressNotWhitelisted(tokenAddress); + if (depositData.optionalMessageCheck == OptionalMessageCheck.Invalid) { + return depositData.message; + } - if (_tokenContractAddressToTokenProperties[tokenAddress].isBurnable) { - mintERC20(tokenAddress, address(recipientAddress), convertToExternalBalance(tokenAddress, amount)); - } else { - releaseERC20(tokenAddress, address(recipientAddress), convertToExternalBalance(tokenAddress, amount)); + if (depositData.optionalMessageCheck == OptionalMessageCheck.Valid) { + (bool success, bytes memory result) = + depositData.recipientAddress.excessivelySafeCall(depositData.gas, 0, maxReturnBytes, depositData.message); + if (!success && !ExcessivelySafeCall.revertWith(result)) revert OptionalMessageCallFailed(); + return abi.encode(depositData.tokenAddress, depositData.recipientAddress, depositData.amount, result); } - return abi.encode(tokenAddress, address(recipientAddress), amount); + + return abi.encode(depositData.tokenAddress, depositData.recipientAddress, depositData.amount); } /** @@ -104,6 +118,7 @@ contract ERC20Handler is IHandler, ERCHandlerHelpers, ERC20Safe { (tokenAddress, recipient, amount) = abi.decode(data, (address, address, uint)); + recipient.mustNotBeZero(); releaseERC20(tokenAddress, recipient, amount); } @@ -114,9 +129,11 @@ contract ERC20Handler is IHandler, ERCHandlerHelpers, ERC20Safe { Sets decimals value for contractAddress if value is provided in args. @param resourceID ResourceID to be used when making deposits. @param contractAddress Address of contract to be called when a deposit is made and a deposited is executed. - @param args Additional data to be passed to specified handler. + @param args Byte array which is either empty if the token contract decimals are the same as the bridge defaultDecimals, + or has a first byte set to the uint8 decimals value of the token contract. */ function setResource(bytes32 resourceID, address contractAddress, bytes calldata args) external onlyBridge { + contractAddress.mustNotBeZero(); _setResource(resourceID, contractAddress); if (args.length > 0) { diff --git a/contracts/handlers/ERC721Handler.sol b/contracts/handlers/ERC721Handler.sol index d030ff98..c5fcaa72 100644 --- a/contracts/handlers/ERC721Handler.sol +++ b/contracts/handlers/ERC721Handler.sol @@ -15,6 +15,7 @@ import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; @notice This contract is intended to be used with the Bridge contract. */ contract ERC721Handler is IHandler, ERCHandlerHelpers, ERC721Safe { + using SanityChecks for *; using ERC165Checker for address; bytes4 private constant _INTERFACE_ERC721_METADATA = 0x5b5e139f; @@ -91,7 +92,8 @@ contract ERC721Handler is IHandler, ERCHandlerHelpers, ERC721Safe { bytes memory metaData; (tokenID, lenDestinationRecipientAddress) = abi.decode(data, (uint, uint)); - offsetMetaData = 64 + lenDestinationRecipientAddress; + lenDestinationRecipientAddress.mustBe(20); + offsetMetaData = 84; destinationRecipientAddress = bytes(data[64:offsetMetaData]); lenMetaData = abi.decode(data[offsetMetaData:], (uint)); metaData = bytes(data[offsetMetaData + 32:offsetMetaData + 32 + lenMetaData]); @@ -128,6 +130,7 @@ contract ERC721Handler is IHandler, ERCHandlerHelpers, ERC721Safe { (tokenAddress, recipient, tokenID) = abi.decode(data, (address, address, uint)); + recipient.mustNotBeZero(); releaseERC721(tokenAddress, address(this), recipient, tokenID); } @@ -140,6 +143,7 @@ contract ERC721Handler is IHandler, ERCHandlerHelpers, ERC721Safe { @param args Additional data to be passed to specified handler. */ function setResource(bytes32 resourceID, address contractAddress, bytes calldata args) external onlyBridge { + contractAddress.mustNotBeZero(); _setResource(resourceID, contractAddress); } } diff --git a/contracts/handlers/ERCHandlerHelpers.sol b/contracts/handlers/ERCHandlerHelpers.sol index 839166e2..88e805ce 100644 --- a/contracts/handlers/ERCHandlerHelpers.sol +++ b/contracts/handlers/ERCHandlerHelpers.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.11; import "../interfaces/IERCHandler.sol"; +import "../utils/SanityChecks.sol"; /** @title Function used across handler contracts. @@ -93,10 +94,9 @@ contract ERCHandlerHelpers is IERCHandler { } /** - @notice Converts token amount based on decimal places difference between the nework - deposit is made on and bridge. + @notice Converts token amount based on decimal places difference from bridge to current network. @param tokenAddress Address of contract to be used when executing proposals. - @param amount Decimals value to be set for {contractAddress}. + @param amount The bridge internal amount with defaultDecimals. */ function convertToExternalBalance(address tokenAddress, uint256 amount) internal view returns(uint256) { Decimals memory decimals = _tokenContractAddressToTokenProperties[tokenAddress].decimals; @@ -110,10 +110,10 @@ contract ERCHandlerHelpers is IERCHandler { } /** - @notice Converts token amount based on decimal places difference between the bridge and nework - deposit is executed on. + @notice Converts token amount based on decimal places difference from current network to bridge. @param tokenAddress Address of contract to be used when executing proposals. - @param amount Decimals value to be set for {contractAddress}. + @param amount The token amount with decimals on the current network. + @return 32-length byte array with internal bridge amount OR empty byte array if conversion is not needed. */ function convertToInternalBalance(address tokenAddress, uint256 amount) internal view returns(bytes memory) { Decimals memory decimals = _tokenContractAddressToTokenProperties[tokenAddress].decimals; @@ -121,7 +121,7 @@ contract ERCHandlerHelpers is IERCHandler { if (!decimals.isSet) { return ""; } else if (decimals.externalDecimals >= defaultDecimals) { - convertedBalance = amount / (10 ** (decimals.externalDecimals - defaultDecimals)); + convertedBalance = amount / (10 ** (decimals.externalDecimals - defaultDecimals)); } else { convertedBalance = amount * (10 ** (defaultDecimals - decimals.externalDecimals)); } diff --git a/contracts/handlers/GmpHandler.sol b/contracts/handlers/GmpHandler.sol index 229acce1..2a63bbc1 100644 --- a/contracts/handlers/GmpHandler.sol +++ b/contracts/handlers/GmpHandler.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.11; import "../interfaces/IHandler.sol"; +import "../utils/SanityChecks.sol"; /** @title Handles generic deposits and deposit executions. @@ -10,6 +11,8 @@ import "../interfaces/IHandler.sol"; @notice This contract is intended to be used with the Bridge contract. */ contract GmpHandler is IHandler { + using SanityChecks for *; + uint256 public constant MAX_FEE = 1000000; address public immutable _bridgeAddress; @@ -165,10 +168,13 @@ contract GmpHandler is IHandler { maxFee = uint256(bytes32(data[:pointer += 32])); lenExecuteFuncSignature = uint16(bytes2(data[pointer:pointer += 2])); + lenExecuteFuncSignature.mustBe(4); executeFuncSignature = bytes4(data[pointer:pointer += lenExecuteFuncSignature]); lenExecuteContractAddress = uint8(bytes1(data[pointer:pointer += 1])); + lenExecuteContractAddress.mustBe(20); executeContractAddress = address(uint160(bytes20(data[pointer:pointer += lenExecuteContractAddress]))); lenExecutionDataDepositor = uint8(bytes1(data[pointer:pointer += 1])); + lenExecutionDataDepositor.mustBe(20); executionDataDepositor = address(uint160(bytes20(data[pointer:pointer += lenExecutionDataDepositor]))); executionData = bytes(data[pointer:]); diff --git a/contracts/handlers/NativeTokenHandler.sol b/contracts/handlers/NativeTokenHandler.sol index 0b8c8815..c3ef03c4 100644 --- a/contracts/handlers/NativeTokenHandler.sol +++ b/contracts/handlers/NativeTokenHandler.sol @@ -3,24 +3,33 @@ pragma solidity 0.8.11; import "../interfaces/IHandler.sol"; +import "../interfaces/ISygmaMessageReceiver.sol"; import "./ERCHandlerHelpers.sol"; +import "./DepositDataHelper.sol"; +import "../utils/ExcessivelySafeCall.sol"; /** @title Handles native token deposits and deposit executions. @author ChainSafe Systems. @notice This contract is intended to be used with the Bridge contract. */ -contract NativeTokenHandler is IHandler, ERCHandlerHelpers { +contract NativeTokenHandler is IHandler, ERCHandlerHelpers, DepositDataHelper { + using SanityChecks for *; + using ExcessivelySafeCall for address; + uint256 internal constant defaultGas = 50000; address public immutable _nativeTokenAdapterAddress; /** @param bridgeAddress Contract address of previously deployed Bridge. + @param nativeTokenAdapterAddress Contract address of previously deployed NativeTokenAdapter. + @param defaultMessageReceiver Contract address of previously deployed DefaultMessageReceiver. */ constructor( address bridgeAddress, - address nativeTokenAdapterAddress - ) ERCHandlerHelpers(bridgeAddress) { + address nativeTokenAdapterAddress, + address defaultMessageReceiver + ) DepositDataHelper(bridgeAddress, defaultMessageReceiver) { _nativeTokenAdapterAddress = nativeTokenAdapterAddress; } @@ -38,24 +47,27 @@ contract NativeTokenHandler is IHandler, ERCHandlerHelpers { @param depositor Address of account making the deposit in the Bridge contract. @param data Consists of {amount} padded to 32 bytes. @notice Data passed into the function should be constructed as follows: - amount uint256 bytes 0 - 32 - destinationRecipientAddress length uint256 bytes 32 - 64 - destinationRecipientAddress bytes bytes 64 - END - @return deposit amount internal representation. + amount uint256 bytes 0 - 32 + destinationRecipientAddress length uint256 bytes 32 - 64 + destinationRecipientAddress bytes bytes 64 - (64 + len(destinationRecipientAddress)) + optionalGas uint256 bytes (64 + len(destinationRecipientAddress)) - (96 + len(destinationRecipientAddress)) + optionalMessage length uint256 bytes (96 + len(destinationRecipientAddress)) - (128 + len(destinationRecipientAddress)) + optionalMessage bytes bytes (160 + len(destinationRecipientAddress)) - END + @return 32-length byte array with internal bridge amount OR empty byte array if conversion is not needed. */ function deposit( bytes32 resourceID, address depositor, bytes calldata data - ) external override onlyBridge returns (bytes memory) { - uint256 amount; + ) external view override onlyBridge returns (bytes memory) { + uint256 amount; (amount) = abi.decode(data, (uint256)); if(depositor != _nativeTokenAdapterAddress) revert InvalidSender(depositor); address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; - return abi.encodePacked(convertToInternalBalance(tokenAddress, amount)); + return convertToInternalBalance(tokenAddress, amount); } /** @@ -65,21 +77,36 @@ contract NativeTokenHandler is IHandler, ERCHandlerHelpers { @param data Consists of {amount}, {lenDestinationRecipientAddress} and {destinationRecipientAddress}. @notice Data passed into the function should be constructed as follows: - amount uint256 bytes 0 - 32 - destinationRecipientAddress length uint256 bytes 32 - 64 // not used - destinationRecipientAddress bytes bytes 64 - 84 + amount uint256 bytes 0 - 32 + destinationRecipientAddress length uint256 bytes 32 - 64 + destinationRecipientAddress bytes bytes 64 - (64 + len(destinationRecipientAddress)) + optionalGas uint256 bytes (64 + len(destinationRecipientAddress)) - (96 + len(destinationRecipientAddress)) + optionalMessage length uint256 bytes (96 + len(destinationRecipientAddress)) - (128 + len(destinationRecipientAddress)) + optionalMessage bytes bytes (160 + len(destinationRecipientAddress)) - END */ function executeProposal(bytes32 resourceID, bytes calldata data) external override onlyBridge returns (bytes memory) { - (uint256 amount) = abi.decode(data, (uint256)); - address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; - address recipientAddress = address(bytes20(bytes(data[64:84]))); - uint256 convertedAmount = convertToExternalBalance(tokenAddress, amount); + DepositData memory depositData = parseDepositData(resourceID, data); - (bool success, ) = address(recipientAddress).call{value: convertedAmount}(""); - if(!success) revert FailedFundsTransfer(); - emit FundsTransferred(recipientAddress, amount); + if (depositData.optionalMessageCheck == OptionalMessageCheck.Invalid) { + return depositData.message; + } + + if (depositData.gas == 0) { + depositData.gas = defaultGas; + } + + (bool success, bytes memory result) = + depositData.recipientAddress.excessivelySafeCall( + depositData.gas, + depositData.externalAmount, + maxReturnBytes, + depositData.message + ); + if (!success && !ExcessivelySafeCall.revertWith(result)) revert FailedFundsTransfer(); + + emit FundsTransferred(depositData.recipientAddress, depositData.externalAmount); - return abi.encode(tokenAddress, address(recipientAddress), convertedAmount); + return abi.encode(depositData.tokenAddress, depositData.recipientAddress, depositData.amount); } /** @@ -97,6 +124,7 @@ contract NativeTokenHandler is IHandler, ERCHandlerHelpers { if (address(this).balance <= amount) revert InsufficientBalance(); (, recipient, amount) = abi.decode(data, (address, address, uint)); + recipient.mustNotBeZero(); (bool success, ) = address(recipient).call{value: amount}(""); if(!success) revert FailedFundsTransfer(); emit Withdrawal(recipient, amount); diff --git a/contracts/handlers/XC20Handler.sol b/contracts/handlers/XC20Handler.sol index 2e1923f4..13847031 100644 --- a/contracts/handlers/XC20Handler.sol +++ b/contracts/handlers/XC20Handler.sol @@ -12,7 +12,9 @@ import "../XC20Safe.sol"; @notice This contract is intended to be used with the Bridge contract. */ contract XC20Handler is IHandler, ERCHandlerHelpers, XC20Safe { - /** + using SanityChecks for *; + + /** @param bridgeAddress Contract address of previously deployed Bridge. */ constructor( @@ -31,7 +33,7 @@ contract XC20Handler is IHandler, ERCHandlerHelpers, XC20Safe { destinationRecipientAddress bytes bytes 64 - END @dev Depending if the corresponding {tokenAddress} for the parsed {resourceID} is marked true in {_tokenContractAddressToTokenProperties[tokenAddress].isBurnable}, deposited tokens will be burned, if not, they will be locked. - @return an empty data. + @return 32-length byte array with internal bridge amount OR empty byte array if conversion is not needed. */ function deposit( bytes32 resourceID, @@ -50,7 +52,7 @@ contract XC20Handler is IHandler, ERCHandlerHelpers, XC20Safe { lockERC20(tokenAddress, depositor, address(this), amount); } - return abi.encodePacked(convertToInternalBalance(tokenAddress, amount)); + return convertToInternalBalance(tokenAddress, amount); } /** @@ -70,7 +72,8 @@ contract XC20Handler is IHandler, ERCHandlerHelpers, XC20Safe { bytes memory destinationRecipientAddress; (amount, lenDestinationRecipientAddress) = abi.decode(data, (uint, uint)); - destinationRecipientAddress = bytes(data[64:64 + lenDestinationRecipientAddress]); + lenDestinationRecipientAddress.mustBe(20); + destinationRecipientAddress = bytes(data[64:84]); bytes20 recipientAddress; address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; @@ -104,6 +107,7 @@ contract XC20Handler is IHandler, ERCHandlerHelpers, XC20Safe { (tokenAddress, recipient, amount) = abi.decode(data, (address, address, uint)); + recipient.mustNotBeZero(); releaseERC20(tokenAddress, recipient, amount); } @@ -116,6 +120,7 @@ contract XC20Handler is IHandler, ERCHandlerHelpers, XC20Safe { @param args Additional data to be passed to specified handler. */ function setResource(bytes32 resourceID, address contractAddress, bytes calldata args) external onlyBridge { + contractAddress.mustNotBeZero(); _setResource(resourceID, contractAddress); uint8 externalTokenDecimals = uint8(bytes1(args)); diff --git a/contracts/handlers/fee/BasicFeeHandler.sol b/contracts/handlers/fee/BasicFeeHandler.sol index c326bfc6..2a7d1c0d 100644 --- a/contracts/handlers/fee/BasicFeeHandler.sol +++ b/contracts/handlers/fee/BasicFeeHandler.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.11; import "../../interfaces/IFeeHandler.sol"; import "../../utils/AccessControl.sol"; +import "../../utils/SanityChecks.sol"; import "../FeeHandlerRouter.sol"; /** @@ -12,6 +13,8 @@ import "../FeeHandlerRouter.sol"; @notice This contract is intended to be used with the Bridge contract. */ contract BasicFeeHandler is IFeeHandler, AccessControl { + using SanityChecks for *; + address public immutable _bridgeAddress; address public immutable _feeHandlerRouterAddress; mapping (uint8 => mapping(bytes32 => uint256)) public _domainResourceIDToFee; @@ -121,6 +124,7 @@ contract BasicFeeHandler is IFeeHandler, AccessControl { function transferFee(address payable[] calldata addrs, uint[] calldata amounts) external onlyAdmin { require(addrs.length == amounts.length, "addrs[], amounts[]: diff length"); for (uint256 i = 0; i < addrs.length; i++) { + addrs[i].mustNotBeZero(); (bool success,) = addrs[i].call{value: amounts[i]}(""); require(success, "Fee ether transfer failed"); emit FeeDistributed(address(0), addrs[i], amounts[i]); diff --git a/contracts/handlers/fee/PercentageERC20FeeHandler.sol b/contracts/handlers/fee/PercentageERC20FeeHandler.sol index 4e2ecd59..7fdca08e 100644 --- a/contracts/handlers/fee/PercentageERC20FeeHandler.sol +++ b/contracts/handlers/fee/PercentageERC20FeeHandler.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.11; import "../../interfaces/IBridge.sol"; import "../../interfaces/IERCHandler.sol"; import "../../ERC20Safe.sol"; +import "../../utils/SanityChecks.sol"; import { BasicFeeHandler } from "./BasicFeeHandler.sol"; /** @@ -13,6 +14,8 @@ import { BasicFeeHandler } from "./BasicFeeHandler.sol"; @notice This contract is intended to be used with the Bridge contract. */ contract PercentageERC20FeeHandler is BasicFeeHandler, ERC20Safe { + using SanityChecks for *; + uint32 public constant HUNDRED_PERCENT = 1e8; /** @@ -133,6 +136,7 @@ contract PercentageERC20FeeHandler is BasicFeeHandler, ERC20Safe { address tokenHandler = IBridge(_bridgeAddress)._resourceIDToHandlerAddress(resourceID); address tokenAddress = IERCHandler(tokenHandler)._resourceIDToTokenContractAddress(resourceID); for (uint256 i = 0; i < addrs.length; i++) { + addrs[i].mustNotBeZero(); releaseERC20(tokenAddress, addrs[i], amounts[i]); emit FeeDistributed(tokenAddress, addrs[i], amounts[i]); } diff --git a/contracts/handlers/fee/dynamic/DynamicGenericFeeHandler.sol b/contracts/handlers/fee/dynamic/DynamicGenericFeeHandler.sol index 9e40995b..d2c71d7f 100644 --- a/contracts/handlers/fee/dynamic/DynamicGenericFeeHandler.sol +++ b/contracts/handlers/fee/dynamic/DynamicGenericFeeHandler.sol @@ -27,11 +27,11 @@ contract TwapGenericFeeHandler is TwapFeeHandler { */ function _calculateFee(address, uint8, uint8 destinationDomainID, bytes32, bytes calldata depositData, bytes calldata) internal view override returns (uint256 fee, address tokenAddress) { uint256 maxFee = uint256(bytes32(depositData[:32])); - uint256 desintationCoinPrice = twapOracle.getPrice(destinationNativeCoinWrap[destinationDomainID]); - if (desintationCoinPrice == 0) revert IncorrectPrice(); + uint256 destinationCoinPrice = twapOracle.getPrice(destinationNativeCoinWrap[destinationDomainID]); + if (destinationCoinPrice == 0) revert IncorrectPrice(); Fee memory destFeeConfig = destinationFee[destinationDomainID]; - uint256 txCost = destFeeConfig.gasPrice * maxFee * desintationCoinPrice / 1e18; + uint256 txCost = destFeeConfig.gasPrice * maxFee * destinationCoinPrice / 1e18; if(destFeeConfig.feeType == ProtocolFeeType.Fixed) { txCost += destFeeConfig.amount; } else if (destFeeConfig.feeType == ProtocolFeeType.Percentage) { diff --git a/contracts/handlers/fee/dynamic/TwapERC20NativeFeeHandler.sol b/contracts/handlers/fee/dynamic/TwapERC20NativeFeeHandler.sol new file mode 100644 index 00000000..9f0aeaa2 --- /dev/null +++ b/contracts/handlers/fee/dynamic/TwapERC20NativeFeeHandler.sol @@ -0,0 +1,25 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +import "./TwapNativeTokenFeeHandler.sol"; + +/** + @title Handles deposit fees based on the destination chain's native coin price provided by Twap oracle. + @author ChainSafe Systems. + @notice This contract is intended to be used with the Bridge contract. + */ +contract TwapERC20NativeFeeHandler is TwapNativeTokenFeeHandler { + + /** + @param bridgeAddress Contract address of previously deployed Bridge. + @param feeHandlerRouterAddress Contract address of previously deployed FeeHandlerRouter. + @param gasUsed Default gas used for proposal execution in the destination. + */ + constructor( + address bridgeAddress, + address feeHandlerRouterAddress, + uint32 gasUsed + ) TwapNativeTokenFeeHandler(bridgeAddress, feeHandlerRouterAddress, gasUsed) { + } +} diff --git a/contracts/handlers/fee/dynamic/TwapFeeHandler.sol b/contracts/handlers/fee/dynamic/TwapFeeHandler.sol index a8f95784..b15d31da 100644 --- a/contracts/handlers/fee/dynamic/TwapFeeHandler.sol +++ b/contracts/handlers/fee/dynamic/TwapFeeHandler.sol @@ -8,6 +8,7 @@ import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../../interfaces/IFeeHandler.sol"; import "../../../interfaces/IERCHandler.sol"; import "../../../interfaces/IBridge.sol"; +import "../../../utils/SanityChecks.sol"; import "./TwapOracle.sol"; /** @@ -16,6 +17,8 @@ import "./TwapOracle.sol"; @notice This contract is intended to be used with the Bridge contract. */ abstract contract TwapFeeHandler is IFeeHandler, AccessControl { + using SanityChecks for *; + address public immutable _bridgeAddress; address public immutable _feeHandlerRouterAddress; @@ -134,9 +137,13 @@ abstract contract TwapFeeHandler is IFeeHandler, AccessControl { /** @notice Sets the fee properties. - @param gasUsed Gas used for transfer. + @param gasUsed Default gas used for proposal execution in the destination. */ function setFeeProperties(uint32 gasUsed) external onlyAdmin { + _setFeeProperties(gasUsed); + } + + function _setFeeProperties(uint32 gasUsed) internal { _gasUsed = gasUsed; emit FeePropertySet(gasUsed); } @@ -187,6 +194,7 @@ abstract contract TwapFeeHandler is IFeeHandler, AccessControl { function transferFee(address payable[] calldata addrs, uint[] calldata amounts) external onlyAdmin { require(addrs.length == amounts.length, "addrs[], amounts[]: diff length"); for (uint256 i = 0; i < addrs.length; i++) { + addrs[i].mustNotBeZero(); (bool success,) = addrs[i].call{value: amounts[i]}(""); require(success, "Fee ether transfer failed"); emit FeeDistributed(address(0), addrs[i], amounts[i]); diff --git a/contracts/handlers/fee/dynamic/TwapNativeTokenFeeHandler.sol b/contracts/handlers/fee/dynamic/TwapNativeTokenFeeHandler.sol index 09faa01c..0a5fa34c 100644 --- a/contracts/handlers/fee/dynamic/TwapNativeTokenFeeHandler.sol +++ b/contracts/handlers/fee/dynamic/TwapNativeTokenFeeHandler.sol @@ -14,22 +14,36 @@ contract TwapNativeTokenFeeHandler is TwapFeeHandler { /** @param bridgeAddress Contract address of previously deployed Bridge. @param feeHandlerRouterAddress Contract address of previously deployed FeeHandlerRouter. + @param gasUsed Default gas used for proposal execution in the destination. */ - constructor(address bridgeAddress, address feeHandlerRouterAddress) TwapFeeHandler(bridgeAddress, feeHandlerRouterAddress) { + constructor( + address bridgeAddress, + address feeHandlerRouterAddress, + uint32 gasUsed + ) TwapFeeHandler(bridgeAddress, feeHandlerRouterAddress) { + _setFeeProperties(gasUsed); } /** @notice Calculates fee for transaction cost. @param destinationDomainID ID of chain deposit will be bridged to. + @param depositData Data to be passed to Native Token handler. @return fee Returns the fee amount. @return tokenAddress Returns the address of the token to be used for fee. */ - function _calculateFee(address, uint8, uint8 destinationDomainID, bytes32, bytes calldata, bytes calldata) internal view override returns (uint256 fee, address tokenAddress) { - uint256 desintationCoinPrice = twapOracle.getPrice(destinationNativeCoinWrap[destinationDomainID]); - if (desintationCoinPrice == 0) revert IncorrectPrice(); + function _calculateFee(address, uint8, uint8 destinationDomainID, bytes32, bytes calldata depositData, bytes calldata) internal view override returns (uint256 fee, address tokenAddress) { + uint256 maxFee = _gasUsed; + uint256 recipientLength = uint256(bytes32(depositData[32:64])); + uint256 pointer = 64 + recipientLength; + if (depositData.length > (pointer + 64)) { + uint256 gas = abi.decode(depositData[pointer:], (uint256)); + maxFee += gas; + } + uint256 destinationCoinPrice = twapOracle.getPrice(destinationNativeCoinWrap[destinationDomainID]); + if (destinationCoinPrice == 0) revert IncorrectPrice(); Fee memory destFeeConfig = destinationFee[destinationDomainID]; - uint256 txCost = destFeeConfig.gasPrice * _gasUsed * desintationCoinPrice / 1e18; + uint256 txCost = destFeeConfig.gasPrice * maxFee * destinationCoinPrice / 1e18; if(destFeeConfig.feeType == ProtocolFeeType.Fixed) { txCost += destFeeConfig.amount; } else if (destFeeConfig.feeType == ProtocolFeeType.Percentage) { diff --git a/contracts/interfaces/ISygmaMessageReceiver.sol b/contracts/interfaces/ISygmaMessageReceiver.sol new file mode 100644 index 00000000..fbd6bb8d --- /dev/null +++ b/contracts/interfaces/ISygmaMessageReceiver.sol @@ -0,0 +1,22 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +/** + @title Interface for optional message receive. + @author ChainSafe Systems. + */ +interface ISygmaMessageReceiver { + /** + @notice ERC20 and NativeToken Handlers will call this function + @notice on recipient if there is an optional message included. + @param token Transferred token. + @param amount Transferred amount. + @param message Arbitrary message. + */ + function handleSygmaMessage( + address token, + uint256 amount, + bytes calldata message + ) external payable; +} diff --git a/contracts/utils/ExcessivelySafeCall.sol b/contracts/utils/ExcessivelySafeCall.sol new file mode 100644 index 00000000..3a3a4764 --- /dev/null +++ b/contracts/utils/ExcessivelySafeCall.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.11; + +// Based on https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol +library ExcessivelySafeCall { + /// @notice Use when you _really_ really _really_ don't trust the called + /// contract. This prevents the called contract from causing reversion of + /// the caller in as many ways as we can. + /// @dev The main difference between this and a solidity low-level call is + /// that we limit the number of bytes that the callee can cause to be + /// copied to caller memory. This prevents stupid things like malicious + /// contracts returning 10,000,000 bytes causing a local OOG when copying + /// to memory. + /// @param _target The address to call + /// @param _gas The amount of gas to forward to the remote contract + /// @param _value The value in wei to send to the remote contract + /// @param _maxCopy The maximum number of bytes of returndata to copy + /// to memory. + /// @param _calldata The data to send to the remote contract + /// @return success and returndata, as `.call()`. Returndata is capped to + /// `_maxCopy` bytes. + function excessivelySafeCall( + address _target, + uint256 _gas, + uint256 _value, + uint16 _maxCopy, + bytes memory _calldata + ) internal returns (bool, bytes memory) { + // set up for assembly call + uint256 _toCopy; + bool _success; + bytes memory _returnData = new bytes(_maxCopy); + // dispatch message to recipient + // by assembly calling "handle" function + // we call via assembly to avoid memcopying a very large returndata + // returned by a malicious contract + assembly { + _success := call( + _gas, // gas + _target, // recipient + _value, // ether value + add(_calldata, 0x20), // inloc + mload(_calldata), // inlen + 0, // outloc + 0 // outlen + ) + // limit our copy to _maxCopy bytes + _toCopy := returndatasize() + if gt(_toCopy, _maxCopy) { + _toCopy := _maxCopy + } + // Store the length of the copied bytes + mstore(_returnData, _toCopy) + // copy the bytes from returndata[0:_toCopy] + returndatacopy(add(_returnData, 0x20), 0, _toCopy) + } + return (_success, _returnData); + } + + /// @notice Use when you _really_ really _really_ don't trust the called + /// contract. This prevents the called contract from causing reversion of + /// the caller in as many ways as we can. + /// @dev The main difference between this and a solidity low-level call is + /// that we limit the number of bytes that the callee can cause to be + /// copied to caller memory. This prevents stupid things like malicious + /// contracts returning 10,000,000 bytes causing a local OOG when copying + /// to memory. + /// @param _target The address to call + /// @param _gas The amount of gas to forward to the remote contract + /// @param _maxCopy The maximum number of bytes of returndata to copy + /// to memory. + /// @param _calldata The data to send to the remote contract + /// @return success and returndata, as `.call()`. Returndata is capped to + /// `_maxCopy` bytes. + function excessivelySafeStaticCall( + address _target, + uint256 _gas, + uint16 _maxCopy, + bytes memory _calldata + ) internal view returns (bool, bytes memory) { + // set up for assembly call + uint256 _toCopy; + bool _success; + bytes memory _returnData = new bytes(_maxCopy); + // dispatch message to recipient + // by assembly calling "handle" function + // we call via assembly to avoid memcopying a very large returndata + // returned by a malicious contract + assembly { + _success := staticcall( + _gas, // gas + _target, // recipient + add(_calldata, 0x20), // inloc + mload(_calldata), // inlen + 0, // outloc + 0 // outlen + ) + // limit our copy to _maxCopy bytes + _toCopy := returndatasize() + if gt(_toCopy, _maxCopy) { + _toCopy := _maxCopy + } + // Store the length of the copied bytes + mstore(_returnData, _toCopy) + // copy the bytes from returndata[0:_toCopy] + returndatacopy(add(_returnData, 0x20), 0, _toCopy) + } + return (_success, _returnData); + } + + /// @dev Inspired by OZ implementation. + function revertWith(bytes memory _returnData) internal pure returns(bool) { + // Look for revert reason and bubble it up if present + if (_returnData.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + assembly { + let returndata_size := mload(_returnData) + revert(add(32, _returnData), returndata_size) + } + } + return false; + } +} \ No newline at end of file diff --git a/contracts/utils/SanityChecks.sol b/contracts/utils/SanityChecks.sol new file mode 100644 index 00000000..50a9fb64 --- /dev/null +++ b/contracts/utils/SanityChecks.sol @@ -0,0 +1,22 @@ +// The Licensed Work is (c) 2024 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +/** + @title Helps validate input parameters. + @author ChainSafe Systems. + */ +library SanityChecks { + error ZeroAddress(); + error UnexpectedValue(uint256 actual, uint256 expected); + + function mustNotBeZero(address addr) internal pure returns(address) { + if (addr == address(0)) revert ZeroAddress(); + return addr; + } + + function mustBe(uint256 actual, uint256 expected) internal pure returns(uint256) { + if (actual != expected) revert UnexpectedValue(actual, expected); + return actual; + } +} diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index fce4212e..79f9dd66 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -9,6 +9,7 @@ const AccessControlSegregatorContract = artifacts.require( ); const PausableContract = artifacts.require("Pausable"); const BridgeContract = artifacts.require("Bridge"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC721HandlerContract = artifacts.require("ERC721Handler"); @@ -44,9 +45,15 @@ module.exports = async function (deployer, network) { ); // deploy handler contracts + const defaultMessageReceiverInstance = await deployer.deploy( + DefaultMessageReceiverContract, + [], + 100000 + ); const erc20HandlerInstance = await deployer.deploy( ERC20HandlerContract, - bridgeInstance.address + bridgeInstance.address, + defaultMessageReceiverInstance.address ); const erc721HandlerInstance = await deployer.deploy( ERC721HandlerContract, @@ -72,10 +79,16 @@ module.exports = async function (deployer, network) { // setup fee router await bridgeInstance.adminChangeFeeHandler(feeRouterInstance.address); + await defaultMessageReceiverInstance.grantRole( + await defaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + erc20HandlerInstance.address + ); + console.table({ "Deployer Address": deployerAddress, "Domain ID": currentNetworkConfig.domainID, "Bridge Address": bridgeInstance.address, + "DefaultMessageReceiver Address": defaultMessageReceiverInstance.address, "ERC20Handler Address": erc20HandlerInstance.address, "ERC721Handler Address": erc721HandlerInstance.address, "FeeRouterContract Address": feeRouterInstance.address, diff --git a/migrations/7_redeploy_token_handlers.js b/migrations/7_redeploy_token_handlers.js index 25247216..17f3d08c 100644 --- a/migrations/7_redeploy_token_handlers.js +++ b/migrations/7_redeploy_token_handlers.js @@ -6,6 +6,7 @@ const parseArgs = require("minimist"); const Utils = require("./utils"); const BridgeContract = artifacts.require("Bridge"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const XC20HandlerContract = artifacts.require("XC20Handler"); @@ -28,6 +29,7 @@ module.exports = async function (deployer, network) { // fetch deployed contracts addresses const bridgeInstance = await BridgeContract.deployed(); + const defaultMessageReceiverInstance = await DefaultMessageReceiverContract.deployed(); const erc20HandlerInstance = await ERC20HandlerContract.deployed(); try { xc20HandlerInstance = await XC20HandlerContract.deployed(); @@ -42,7 +44,8 @@ module.exports = async function (deployer, network) { bridgeInstance, ERC20HandlerContract, erc20HandlerInstance, - TOKEN_TYPE.ERC20 + TOKEN_TYPE.ERC20, + defaultMessageReceiverInstance ); // redeploy XC20 handler and register (if deployed to current network) diff --git a/migrations/8_renounceAdmin.js b/migrations/8_renounceAdmin.js index 3c85540e..82e87b37 100644 --- a/migrations/8_renounceAdmin.js +++ b/migrations/8_renounceAdmin.js @@ -9,6 +9,7 @@ const AccessControlSegregatorContract = artifacts.require( const FeeRouterContract = artifacts.require("FeeHandlerRouter"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); module.exports = async function (deployer, network) { const networksConfig = Utils.getNetworksConfig(); @@ -22,6 +23,7 @@ module.exports = async function (deployer, network) { const feeRouterInstance = await FeeRouterContract.deployed(); const basicFeeHandlerInstance = await BasicFeeHandlerContract.deployed(); const percentageFeeHandlerInstance = await PercentageFeeHandlerContract.deployed(); + const defaultMessageReceiverInstance = await DefaultMessageReceiverContract.deployed(); if (currentNetworkConfig.access.feeHandlerAdmin) { console.log( @@ -53,6 +55,22 @@ module.exports = async function (deployer, network) { ); } + if (currentNetworkConfig.access.defaultMessageReceiverAdmin) { + console.log( + "Renouncing default message receiver admin to %s", + currentNetworkConfig.access.defaultMessageReceiverAdmin + ); + + await defaultMessageReceiverInstance.grantRole( + "0x00", + currentNetworkConfig.access.defaultMessageReceiverAdmin + ); + await defaultMessageReceiverInstance.renounceRole( + "0x00", + await Utils.getDeployerAddress(deployer) + ); + } + for (const [func, admin] of Object.entries( currentNetworkConfig.access.accessControl )) { diff --git a/migrations/mainnet.json b/migrations/mainnet.json index b6eb2293..500dbc0e 100644 --- a/migrations/mainnet.json +++ b/migrations/mainnet.json @@ -6,6 +6,7 @@ "access": { "feeRouterAdmin": "0xde79695d5cefF7c324552B3ecbe6165f77FCdF53", "feeHandlerAdmin": "0xde79695d5cefF7c324552B3ecbe6165f77FCdF53", + "defaultMessageReceiverAdmin": "0xde79695d5cefF7c324552B3ecbe6165f77FCdF53", "accessControl": { "0x80ae1c28": "0x695bd50CB07ffBd4098b272CE8b52B3c256ca049", "0xffaac0eb": "0x695bd50CB07ffBd4098b272CE8b52B3c256ca049", diff --git a/migrations/testnet.json b/migrations/testnet.json index fde3e0bb..01fb0e1d 100644 --- a/migrations/testnet.json +++ b/migrations/testnet.json @@ -6,6 +6,7 @@ "access": { "feeRouterAdmin": "0x8616909A3a1DbdA36cB3f145EE978dB70041e816", "feeHandlerAdmin": "0x8616909A3a1DbdA36cB3f145EE978dB70041e816", + "defaultMessageReceiverAdmin": "0xde79695d5cefF7c324552B3ecbe6165f77FCdF53", "accessControl": { "0x80ae1c28": "0x8616909A3a1DbdA36cB3f145EE978dB70041e816", "0xffaac0eb": "0x8616909A3a1DbdA36cB3f145EE978dB70041e816", @@ -67,6 +68,7 @@ "access": { "feeRouterAdmin": "0xe9f23A8289764280697a03aC06795eA92a170e42", "feeHandlerAdmin": "0xe9f23A8289764280697a03aC06795eA92a170e42", + "defaultMessageReceiverAdmin": "0xde79695d5cefF7c324552B3ecbe6165f77FCdF53", "accessControl": { "0x80ae1c28": "0xe9f23A8289764280697a03aC06795eA92a170e42", "0xffaac0eb": "0xe9f23A8289764280697a03aC06795eA92a170e42", diff --git a/migrations/utils.js b/migrations/utils.js index 408761fc..74864113 100644 --- a/migrations/utils.js +++ b/migrations/utils.js @@ -190,7 +190,8 @@ async function redeployHandler( bridgeInstance, handlerContract, handlerInstance, - tokenType + tokenType, + defaultMessageReceiverInstance ) { let deployNewHandler = true; let newHandlerInstance; @@ -199,7 +200,8 @@ async function redeployHandler( if (deployNewHandler) { newHandlerInstance = await deployer.deploy( handlerContract, - bridgeInstance.address + bridgeInstance.address, + defaultMessageReceiverInstance && defaultMessageReceiverInstance.address ); console.log("New handler address:", "\t", newHandlerInstance.address); deployNewHandler = false; @@ -210,7 +212,8 @@ async function redeployHandler( erc20, bridgeInstance, handlerInstance, - newHandlerInstance + newHandlerInstance, + defaultMessageReceiverInstance ); } } @@ -220,7 +223,8 @@ async function migrateToNewTokenHandler( tokenConfig, bridgeInstance, handlerInstance, - newHandlerInstance + newHandlerInstance, + defaultMessageReceiverInstance ) { const tokenContractAddress = await handlerInstance._resourceIDToTokenContractAddress( tokenConfig.resourceID @@ -234,8 +238,8 @@ async function migrateToNewTokenHandler( tokenConfig.decimals != "18" ? tokenConfig.decimals : emptySetResourceData ); - if(tokenConfig.strategy === "mb"){ - const erc20Instance = await ERC20PresetMinterPauser.at(tokenContractAddress); + if(tokenConfig.strategy === "mb") { + const erc20Instance = await ERC20PresetMinterPauser.at(tokenContractAddress); await erc20Instance.grantRole( await erc20Instance.MINTER_ROLE(), @@ -248,6 +252,12 @@ async function migrateToNewTokenHandler( ); } + if (defaultMessageReceiverInstance) { + await defaultMessageReceiverInstance.grantRole( + await defaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + newHandlerInstance.address + ); + } console.log("Associated resourceID:", "\t", tokenConfig.resourceID); console.log( "-------------------------------------------------------------------------------" diff --git a/package.json b/package.json index 3e2aabf6..ddc44264 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build/contracts/TwapFeeHandler.json", "build/contracts/FeeHandlerRouter.json", "build/contracts/NativeTokenAdapter.json", - "build/contracts/NativeTokenHandler.json", + "build/contracts/DefaultMessageReceiver.json", "contracts/interfaces" ], "directories": { diff --git a/test/adapters/native/collectFee.js b/test/adapters/native/collectFee.js index 666e14f8..035f302f 100644 --- a/test/adapters/native/collectFee.js +++ b/test/adapters/native/collectFee.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); @@ -25,6 +26,7 @@ contract("Bridge - [collect fee - native token]", async (accounts) => { const transferredAmount = depositAmount.sub(fee); let BridgeInstance; + let DefaultMessageReceiverInstance; let NativeTokenHandlerInstance; let BasicFeeHandlerInstance; let FeeHandlerRouterInstance; @@ -50,9 +52,11 @@ contract("Bridge - [collect fee - native token]", async (accounts) => { BridgeInstance.address, resourceID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, ); await BridgeInstance.adminSetResource( diff --git a/test/adapters/native/decimalConversion.js b/test/adapters/native/decimalConversion.js index b325d3e4..3eeeb058 100644 --- a/test/adapters/native/decimalConversion.js +++ b/test/adapters/native/decimalConversion.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); @@ -36,6 +37,7 @@ contract("Bridge - [decimal conversion - native token]", async (accounts) => { ); let BridgeInstance; + let DefaultMessageReceiverInstance; let NativeTokenHandlerInstance; let BasicFeeHandlerInstance; let FeeHandlerRouterInstance; @@ -62,9 +64,11 @@ contract("Bridge - [decimal conversion - native token]", async (accounts) => { BridgeInstance.address, resourceID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, ); await BridgeInstance.adminSetResource( @@ -172,6 +176,16 @@ contract("Bridge - [decimal conversion - native token]", async (accounts) => { {from: relayer1Address} ); + const internalHandlerTx = await TruffleAssert.createTransactionResult( + NativeTokenHandlerInstance, + proposalTx.tx + ); + TruffleAssert.eventEmitted(internalHandlerTx, "FundsTransferred", (event) => { + return ( + event.amount.toNumber() === expectedRecipientTransferAmount.toNumber() + ); + }); + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { return ( event.originDomainID.toNumber() === originDomainID && @@ -179,7 +193,7 @@ contract("Bridge - [decimal conversion - native token]", async (accounts) => { event.dataHash === dataHash && event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( ["address", "address", "uint256"], - [NativeTokenHandlerInstance.address, evmRecipientAddress, expectedRecipientTransferAmount] + [NativeTokenHandlerInstance.address, evmRecipientAddress, convertedTransferAmount] ) ); }); diff --git a/test/adapters/native/deposit.js b/test/adapters/native/deposit.js index 8fb1b674..96c5da31 100644 --- a/test/adapters/native/deposit.js +++ b/test/adapters/native/deposit.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); @@ -26,6 +27,7 @@ contract("Bridge - [deposit - native token]", async (accounts) => { const transferredAmount = depositAmount.sub(fee); let BridgeInstance; + let DefaultMessageReceiverInstance; let NativeTokenHandlerInstance; let BasicFeeHandlerInstance; let FeeHandlerRouterInstance; @@ -51,9 +53,11 @@ contract("Bridge - [deposit - native token]", async (accounts) => { BridgeInstance.address, resourceID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, ); await BridgeInstance.adminSetResource( @@ -146,6 +150,7 @@ contract("Bridge - [deposit - native token]", async (accounts) => { const NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, invalidAdapterAddress, + DefaultMessageReceiverInstance.address, ); await BridgeInstance.adminSetResource( diff --git a/test/adapters/native/distributeFee.js b/test/adapters/native/distributeFee.js index 5d9fd288..15bba3c5 100644 --- a/test/adapters/native/distributeFee.js +++ b/test/adapters/native/distributeFee.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers.js"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); @@ -36,6 +37,7 @@ contract("Native token adapter - [distributeFee]", async (accounts) => { }; let BridgeInstance; + let DefaultMessageReceiverInstance; let NativeTokenHandlerInstance; let NativeTokenAdapterInstance; let FeeHandlerRouterInstance; @@ -52,8 +54,13 @@ contract("Native token adapter - [distributeFee]", async (accounts) => { FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( BridgeInstance.address, @@ -68,6 +75,7 @@ contract("Native token adapter - [distributeFee]", async (accounts) => { NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, ); await BridgeInstance.adminSetResource( diff --git a/test/adapters/native/executeProposal.js b/test/adapters/native/executeProposal.js index 651c45a6..d40d1632 100644 --- a/test/adapters/native/executeProposal.js +++ b/test/adapters/native/executeProposal.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); @@ -28,6 +29,7 @@ contract("Bridge - [execute proposal - native token]", async (accounts) => { const transferredAmount = depositAmount.sub(fee); let BridgeInstance; + let DefaultMessageReceiverInstance; let NativeTokenHandlerInstance; let BasicFeeHandlerInstance; let FeeHandlerRouterInstance; @@ -56,9 +58,11 @@ contract("Bridge - [execute proposal - native token]", async (accounts) => { BridgeInstance.address, resourceID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, ); await BridgeInstance.adminSetResource( diff --git a/test/adapters/native/optionalContractCall/collectFee.js b/test/adapters/native/optionalContractCall/collectFee.js new file mode 100644 index 00000000..9300a717 --- /dev/null +++ b/test/adapters/native/optionalContractCall/collectFee.js @@ -0,0 +1,162 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract("Bridge - [collect fee - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = Ethers.utils.parseEther("1"); + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = depositAmount.sub(fee); + const executionGasAmount = 30000000; + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let message; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + Ethers.constants.AddressZero, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [evmRecipientAddress, "20"]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("Native token fee should be successfully deducted", async () => { + const depositorBalanceBefore = await web3.eth.getBalance(depositorAddress); + const adapterBalanceBefore = await web3.eth.getBalance(NativeTokenAdapterInstance.address); + const handlerBalanceBefore = await web3.eth.getBalance(NativeTokenHandlerInstance.address); + + await TruffleAssert.passes( + NativeTokenAdapterInstance.depositToEVMWithMessage( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount, + } + )); + + // check that correct ETH amount is successfully transferred to the adapter + const adapterBalanceAfter = await web3.eth.getBalance(NativeTokenAdapterInstance.address); + const handlerBalanceAfter = await web3.eth.getBalance(NativeTokenHandlerInstance.address); + assert.strictEqual( + new Ethers.BigNumber.from(transferredAmount).add(handlerBalanceBefore).toString(), handlerBalanceAfter + ); + + // check that adapter funds are transferred to the native handler contracts + assert.strictEqual( + adapterBalanceBefore, + adapterBalanceAfter + ); + + // check that depositor before and after balances align + const depositorBalanceAfter = await web3.eth.getBalance(depositorAddress); + expect( + Number(Ethers.utils.formatEther(new Ethers.BigNumber.from(depositorBalanceBefore).sub(depositAmount))) + ).to.be.within( + Number(Ethers.utils.formatEther(depositorBalanceAfter))*0.99, + Number(Ethers.utils.formatEther(depositorBalanceAfter))*1.01 + ) + }); +}); diff --git a/test/adapters/native/optionalContractCall/decimalConversion.js b/test/adapters/native/optionalContractCall/decimalConversion.js new file mode 100644 index 00000000..f86d27bc --- /dev/null +++ b/test/adapters/native/optionalContractCall/decimalConversion.js @@ -0,0 +1,255 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract("Bridge - [decimal conversion - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const originDecimalPlaces = 8; + const depositAmount = Ethers.utils.parseUnits("1", originDecimalPlaces); + const fee = Ethers.utils.parseUnits("0.1", originDecimalPlaces); + const transferredAmount = depositAmount.sub(fee); + const convertedTransferAmount = Ethers.utils.parseEther("0.9"); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + + + const AbiCoder = new Ethers.utils.AbiCoder(); + const expectedHandlerResponse = AbiCoder.encode( + ["uint256"], + [convertedTransferAmount] + ); + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let depositProposalData; + let ERC20MintableInstance; + + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + Ethers.constants.AddressZero, + originDecimalPlaces + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + NativeTokenHandlerInstance.address + ); + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [evmRecipientAddress, "20"]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: NativeTokenHandlerInstance.address, + value: "1000000000000000000" + }) + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("[sanity] decimals value is set if args are provided to 'adminSetResource'", async () => { + const NativeTokenDecimals = (await NativeTokenHandlerInstance._tokenContractAddressToTokenProperties.call( + Ethers.constants.AddressZero + )).decimals; + + assert.strictEqual(NativeTokenDecimals.isSet, true); + assert.strictEqual(NativeTokenDecimals["externalDecimals"], "8"); + }); + + it("Deposit converts sent token amount with 8 decimals to 18 decimal places", async () => { + const depositTx = await NativeTokenAdapterInstance.depositToEVMWithMessage( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + } + ); + + await TruffleAssert.passes(depositTx); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === NativeTokenAdapterInstance.address && + event.data === depositProposalData && + event.handlerResponse === expectedHandlerResponse + ); + }); + }); + + it("Proposal execution converts sent token amount with 18 decimals to 8 decimal places", async () => { + const expectedRecipientTransferAmount = Ethers.utils.parseUnits("0.9", originDecimalPlaces); + const proposalData = Helpers.createOptionalContractCallDepositData( + convertedTransferAmount, // 18 decimals + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + const dataHash = Ethers.utils.keccak256( + NativeTokenHandlerInstance.address + proposalData.substr(2) + ); + + const proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: proposalData, + }; + + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + const recipientBalanceBefore = await web3.eth.getBalance(evmRecipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + const internalHandlerTx = await TruffleAssert.createTransactionResult( + NativeTokenHandlerInstance, + proposalTx.tx + ); + TruffleAssert.eventEmitted(internalHandlerTx, "FundsTransferred", (event) => { + return ( + event.amount.toNumber() === expectedRecipientTransferAmount.toNumber() + ); + }); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256"], + [Ethers.constants.AddressZero, DefaultMessageReceiverInstance.address, convertedTransferAmount] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that tokens are transferred to recipient address + const recipientBalanceAfter = await web3.eth.getBalance(evmRecipientAddress); + assert.strictEqual(transferredAmount.add(recipientBalanceBefore).toString(), recipientBalanceAfter); + }); +}); diff --git a/test/adapters/native/optionalContractCall/deposit.js b/test/adapters/native/optionalContractCall/deposit.js new file mode 100644 index 00000000..3970e293 --- /dev/null +++ b/test/adapters/native/optionalContractCall/deposit.js @@ -0,0 +1,272 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + + +contract("Bridge - [deposit - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const btcRecipientAddress = "bc1qs0fcdq73vgurej48yhtupzcv83un2p5qhsje7n"; + const depositAmount = Ethers.utils.parseEther("1"); + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = depositAmount.sub(fee); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let ERC20MintableInstance; + let message; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, + ); + + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [evmRecipientAddress, "20"]), + }]; + message = Helpers.createMessageCallData( + transactionId, + actions, + DefaultMessageReceiverInstance.address + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("Native token deposit can be made", async () => { + await TruffleAssert.passes( + await NativeTokenAdapterInstance.deposit( + destinationDomainID, + btcRecipientAddress, + { + from: depositorAddress, + value: depositAmount, + } + ) + ); + }); + + it("Native token deposit to EVM can be made", async () => { + await TruffleAssert.passes( + await NativeTokenAdapterInstance.depositToEVM( + destinationDomainID, + evmRecipientAddress, + { + from: depositorAddress, + value: depositAmount, + } + ) + ); + }); + + it("Native token deposit to EVM with message can be made", async () => { + await TruffleAssert.passes( + await NativeTokenAdapterInstance.depositToEVMWithMessage( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount, + } + ) + ); + }); + + it("Native token general deposit can be made", async () => { + const addressLength = 20; + const depositData = Helpers.abiEncode(["uint256", "address"], [addressLength, evmRecipientAddress]) + await TruffleAssert.passes( + await NativeTokenAdapterInstance.depositGeneral( + destinationDomainID, + depositData, + { + from: depositorAddress, + value: depositAmount, + } + ) + ); + }); + + it("_depositCounts should be increments from 0 to 1", async () => { + await NativeTokenAdapterInstance.deposit( + destinationDomainID, + btcRecipientAddress, + { + from: depositorAddress, + value: depositAmount, + } + ); + + const depositCount = await BridgeInstance._depositCounts.call( + destinationDomainID + ); + assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); + }); + + it("Deposit event is fired with expected value", async () => { + const depositTx = await NativeTokenAdapterInstance.deposit( + destinationDomainID, + btcRecipientAddress, + { + from: depositorAddress, + value: depositAmount, + } + ); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + const depositData = Helpers.createBtcDepositData(transferredAmount, btcRecipientAddress); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === NativeTokenAdapterInstance.address && + event.data === depositData && + event.handlerResponse === null + ); + }); + }); + + it("Should revert if destination domain is current bridge domain", async () => { + await Helpers.reverts( + NativeTokenAdapterInstance.deposit(originDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + }); + + it("Should revert if sender is not native token adapter", async () => { + const invalidAdapterAddress = accounts[2]; + const NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + invalidAdapterAddress, + DefaultMessageReceiverInstance.address, + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + + await Helpers.reverts( + NativeTokenAdapterInstance.deposit(destinationDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + }); + + it("Should revert if execution gas provided is 0", async () => { + const invalidExecutionGasAmount = 0; + await Helpers.expectToRevertWithCustomError( + NativeTokenAdapterInstance.depositToEVMWithMessage.call( + destinationDomainID, + Ethers.constants.AddressZero, + invalidExecutionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount, + } + ), + "ZeroGas()" + ); + }); + + it("Should revert if msg.value is 0", async () => { + await Helpers.expectToRevertWithCustomError( + NativeTokenAdapterInstance.depositToEVMWithMessage.call( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + } + ), + "InsufficientMsgValueAmount(uint256)" + ); + }); +}); diff --git a/test/adapters/native/optionalContractCall/distributeFee.js b/test/adapters/native/optionalContractCall/distributeFee.js new file mode 100644 index 00000000..de781915 --- /dev/null +++ b/test/adapters/native/optionalContractCall/distributeFee.js @@ -0,0 +1,285 @@ +// 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.js"); + +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract("Native token adapter - [distributeFee]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[2]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = Ethers.utils.parseEther("1"); + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = depositAmount.sub(fee); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + + + const assertOnlyAdmin = (method, ...params) => { + return TruffleAssert.fails( + method(...params, {from: accounts[1]}), + "sender doesn't have admin role" + ); + }; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let NativeTokenHandlerInstance; + let NativeTokenAdapterInstance; + let FeeHandlerRouterInstance; + let BasicFeeHandlerInstance; + let ERC20MintableInstance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + accounts[0] + )) + ]); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [recipientAddress, "20"]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + recipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("should distribute fees", async () => { + await BridgeInstance.adminChangeFeeHandler(BasicFeeHandlerInstance.address); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + assert.equal( + web3.utils.fromWei(await BasicFeeHandlerInstance._domainResourceIDToFee( + destinationDomainID, + resourceID + ), "ether"), + Ethers.utils.formatUnits(fee) + ); + + // check the balance is 0 + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(BridgeInstance.address), + "ether" + ), + "0" + ); + await NativeTokenAdapterInstance.depositToEVMWithMessage( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + } + ); + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(BridgeInstance.address), + "ether" + ), + "0" + ); + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(NativeTokenAdapterInstance.address), + "ether" + ), + "0" + ); + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(NativeTokenHandlerInstance.address), + "ether" + ), + Ethers.utils.formatUnits(transferredAmount) + ); + + const b1Before = await web3.eth.getBalance(accounts[1]); + const b2Before = await web3.eth.getBalance(accounts[2]); + + const payout = Ethers.utils.parseEther("0.01"); + // Transfer the funds + const tx = await BasicFeeHandlerInstance.transferFee( + [accounts[1], accounts[2]], + [payout, payout] + ); + TruffleAssert.eventEmitted(tx, "FeeDistributed", (event) => { + return ( + event.tokenAddress === "0x0000000000000000000000000000000000000000" && + event.recipient === accounts[1] && + event.amount.toString() === payout.toString() + ); + }); + TruffleAssert.eventEmitted(tx, "FeeDistributed", (event) => { + return ( + event.tokenAddress === "0x0000000000000000000000000000000000000000" && + event.recipient === accounts[2] && + event.amount.toString() === payout.toString() + ); + }); + b1 = await web3.eth.getBalance(accounts[1]); + b2 = await web3.eth.getBalance(accounts[2]); + assert.equal(b1, Ethers.BigNumber.from(b1Before).add(payout)); + assert.equal(b2, Ethers.BigNumber.from(b2Before).add(payout)); + }); + + it("should require admin role to distribute fee", async () => { + await BridgeInstance.adminChangeFeeHandler(BasicFeeHandlerInstance.address); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + + await NativeTokenAdapterInstance.depositToEVMWithMessage( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + } + ); + + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(NativeTokenAdapterInstance.address), + "ether" + ), + "0" + ); + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(NativeTokenHandlerInstance.address), + "ether" + ), + Ethers.utils.formatUnits(transferredAmount) + ); + + const payout = Ethers.utils.parseEther("0.01"); + await assertOnlyAdmin( + BasicFeeHandlerInstance.transferFee, + [accounts[3], accounts[4]], + [payout, payout] + ); + }); + + it("should revert if addrs and amounts arrays have different length", async () => { + await BridgeInstance.adminChangeFeeHandler(BasicFeeHandlerInstance.address); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + + await NativeTokenAdapterInstance.depositToEVMWithMessage( + destinationDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + } + ); + + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(NativeTokenAdapterInstance.address), + "ether" + ), + "0" + ); + assert.equal( + web3.utils.fromWei( + await web3.eth.getBalance(NativeTokenHandlerInstance.address), + "ether" + ), + Ethers.utils.formatUnits(transferredAmount) + ); + + const payout = Ethers.utils.parseEther("0.01"); + await TruffleAssert.fails( + BasicFeeHandlerInstance.transferFee( + [accounts[3], accounts[4]], + [payout, payout, payout] + ), + "addrs[], amounts[]: diff length" + ); + }); +}); diff --git a/test/adapters/native/optionalContractCall/executeProposal.js b/test/adapters/native/optionalContractCall/executeProposal.js new file mode 100644 index 00000000..f903b32e --- /dev/null +++ b/test/adapters/native/optionalContractCall/executeProposal.js @@ -0,0 +1,409 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract("Bridge - [execute proposal - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = Ethers.utils.parseEther("1"); + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = depositAmount.sub(fee); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + const amountToMint = 20; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let ERC20MintableInstance; + let proposal; + let depositProposalData; + let dataHash; + let message; + + 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 + ); + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, + ); + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + NativeTokenHandlerInstance.address + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + Ethers.constants.AddressZero, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [evmRecipientAddress, "20"]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + dataHash = Ethers.utils.keccak256( + NativeTokenHandlerInstance.address + depositProposalData.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 with contract call successfully", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + const recipientNativeBalanceBefore = await web3.eth.getBalance(evmRecipientAddress); + const recipientERC20BalanceBefore = await ERC20MintableInstance.balanceOf(evmRecipientAddress); + const defaultReceiverBalanceBefore = await web3.eth.getBalance(DefaultMessageReceiverInstance.address); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + gas: executionGasAmount + }) + ); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that tokens are transferred to recipient address + const recipientNativeBalanceAfter = await web3.eth.getBalance(evmRecipientAddress); + const recipientERC20BalanceAfter = await ERC20MintableInstance.balanceOf(evmRecipientAddress); + const defaultReceiverBalanceAfter = await web3.eth.getBalance(DefaultMessageReceiverInstance.address); + + assert.strictEqual( + transferredAmount.add(recipientNativeBalanceBefore).toString(), + recipientNativeBalanceAfter + ); + assert.strictEqual(new Ethers.BigNumber.from(amountToMint).add( + recipientERC20BalanceBefore.toString()).toString(), recipientERC20BalanceAfter.toString() + ); + assert.strictEqual(defaultReceiverBalanceBefore.toString(), defaultReceiverBalanceAfter.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( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + gas: executionGasAmount + }) + ); + + const skipExecuteTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + // 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( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + const recipientBalanceBefore = await web3.eth.getBalance(evmRecipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256"], + [Ethers.constants.AddressZero, DefaultMessageReceiverInstance.address, transferredAmount] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + + // check that tokens are transferred to recipient address + const recipientBalanceAfter = await web3.eth.getBalance(evmRecipientAddress); + assert.strictEqual(transferredAmount.add(recipientBalanceBefore).toString(), 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( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + await Helpers.expectToRevertWithCustomError( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }), + "InvalidProposalSigner()" + ); + }); + + it("should revert if handler does not have SYGMA_HANDLER_ROLE", async () => { + await DefaultMessageReceiverInstance.revokeRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + NativeTokenHandlerInstance.address + ); + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + const executeTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + TruffleAssert.eventEmitted(executeTx, "FailedHandlerExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.lowLevelData === "0xdeda9030" // InsufficientPermission() + ); + }); + }); + + it("should revert if insufficient gas limit left for executing action", async () => { + const insufficientExecutionGasAmount = 100000; + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + insufficientExecutionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + const executeTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: insufficientExecutionGasAmount + } + ); + + TruffleAssert.eventEmitted(executeTx, "FailedHandlerExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.lowLevelData === "0x60ee1247" // InsufficientGasLimit() + ); + }); + }); +}); diff --git a/test/contractBridge/admin.js b/test/contractBridge/admin.js index 2fed8933..77838c85 100644 --- a/test/contractBridge/admin.js +++ b/test/contractBridge/admin.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC1155HandlerContract = artifacts.require("ERC1155Handler"); const ERC1155MintableContract = artifacts.require("ERC1155PresetMinterPauser"); @@ -181,8 +182,13 @@ contract("Bridge - [admin]", async (accounts) => { ERC20MintableInstance.address, domainID ); + const DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); assert.equal( @@ -215,8 +221,13 @@ contract("Bridge - [admin]", async (accounts) => { ERC20MintableInstance.address, domainID ); + const DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await TruffleAssert.passes( @@ -300,8 +311,13 @@ contract("Bridge - [admin]", async (accounts) => { ERC20MintableInstance.address, domainID ); - const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await TruffleAssert.passes( @@ -351,8 +367,13 @@ contract("Bridge - [admin]", async (accounts) => { ERC20MintableInstance.address, domainID ); - const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await TruffleAssert.passes( diff --git a/test/contractBridge/depositERC20.js b/test/contractBridge/depositERC20.js index f66ed83b..19953896 100644 --- a/test/contractBridge/depositERC20.js +++ b/test/contractBridge/depositERC20.js @@ -7,6 +7,7 @@ const Helpers = require("../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); const ERC20MintableContractMock = artifacts.require("ERC20PresetMinterPauserMock"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("Bridge - [deposit - ERC20]", async (accounts) => { @@ -54,8 +55,13 @@ contract("Bridge - [deposit - ERC20]", async (accounts) => { initialResourceIDs = [resourceID1, resourceID2]; + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); OriginERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await Promise.all([ diff --git a/test/contractBridge/executeProposalERC20.js b/test/contractBridge/executeProposalERC20.js index 9fa132e0..91a71186 100644 --- a/test/contractBridge/executeProposalERC20.js +++ b/test/contractBridge/executeProposalERC20.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("Bridge - [execute proposal - ERC20]", async (accounts) => { @@ -56,8 +57,13 @@ contract("Bridge - [execute proposal - ERC20]", async (accounts) => { initialContractAddresses = [ERC20MintableInstance.address]; burnableContractAddresses = []; + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await Promise.all([ diff --git a/test/contractBridge/executeProposals.js b/test/contractBridge/executeProposals.js index a3d69727..79d0ecc9 100644 --- a/test/contractBridge/executeProposals.js +++ b/test/contractBridge/executeProposals.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); const ERC721HandlerContract = artifacts.require("ERC721Handler"); @@ -88,8 +89,13 @@ contract("Bridge - [execute proposals]", async (accounts) => { ERC1155MintableInstance.address, ]; + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); ERC721HandlerInstance = await ERC721HandlerContract.new( BridgeInstance.address diff --git a/test/defaultMessageReceiver/direct.js b/test/defaultMessageReceiver/direct.js new file mode 100644 index 00000000..0dd8dabf --- /dev/null +++ b/test/defaultMessageReceiver/direct.js @@ -0,0 +1,537 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); + +const Helpers = require("../helpers"); + +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const TestForwarderContract = artifacts.require("TestForwarder"); + +contract("DefaultMessageReceiver - direct interaction", async (accounts) => { + const adminAddress = accounts[0]; + const handlerAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000111"; + const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + let DefaultMessageReceiverInstance; + let ERC20MintableInstance; + let TestForwarderInstance; + let TestForwarderInstance2; + let SYGMA_HANDLER_ROLE; + + beforeEach(async () => { + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([handlerAddress], 100000); + SYGMA_HANDLER_ROLE = await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(); + + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + TestForwarderInstance = await TestForwarderContract.new(); + TestForwarderInstance2 = await TestForwarderContract.new(); + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + adminAddress + ); + }); + + it("should have valid defaults", async () => { + assert.equal(await DefaultMessageReceiverInstance._recoverGas(), 100000); + assert.isTrue(await DefaultMessageReceiverInstance.hasRole(SYGMA_HANDLER_ROLE, handlerAddress)); + }); + + it("should revert if caller doesn't have sygma handler role", async () => { + await Helpers.expectToRevertWithCustomError( + DefaultMessageReceiverInstance.handleSygmaMessage.call(ZERO_ADDRESS, 0, "0x", { + from: adminAddress, + }), + "InsufficientPermission()" + ); + }); + + it("should revert on performActions if caller is not itself", async () => { + await Helpers.expectToRevertWithCustomError( + DefaultMessageReceiverInstance.performActions.call(ZERO_ADDRESS, ZERO_ADDRESS, 0, [], { + from: adminAddress, + }), + "InsufficientPermission()" + ); + }); + + it("should revert on transferBalanceAction if caller is not itself", async () => { + await Helpers.expectToRevertWithCustomError( + DefaultMessageReceiverInstance.transferBalanceAction.call(ZERO_ADDRESS, ZERO_ADDRESS, { + from: adminAddress, + }), + "InsufficientPermission()" + ); + }); + + it("should revert if message encoding is invalid", async () => { + await Helpers.reverts( + DefaultMessageReceiverInstance.handleSygmaMessage(ZERO_ADDRESS, 0, "0x11", { + from: handlerAddress, + }) + ); + }); + + it("should revert if insufficient gas limit left for executing action", async () => { + const actions = []; + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + await Helpers.expectToRevertWithCustomError( + DefaultMessageReceiverInstance.handleSygmaMessage.call(ZERO_ADDRESS, 0, message, { + from: handlerAddress, + gas: 100000, + }), + "InsufficientGasLimit()" + ); + }); + + it("should pass without actions", async () => { + const actions = []; + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + await DefaultMessageReceiverInstance.handleSygmaMessage(ZERO_ADDRESS, 0, message, { + from: handlerAddress, + gas: 200000, + }); + }); + + it("should not return native token if not received during handling", async () => { + const actions = []; + await web3.eth.sendTransaction({ + from: adminAddress, + to: DefaultMessageReceiverInstance.address, + value: 100, + }); + const message = Helpers.createMessageCallData( + transactionId, + actions, + TestForwarderInstance.address // will revert if received native + ); + await DefaultMessageReceiverInstance.handleSygmaMessage(ZERO_ADDRESS, 0, message, { + from: handlerAddress, + gas: 200000, + }); + assert.equal(await web3.eth.getBalance(DefaultMessageReceiverInstance.address), 100); + }); + + it("should return full native token balance if contract balance increased during handling", async () => { + const actions = []; + await web3.eth.sendTransaction({ + from: adminAddress, + to: DefaultMessageReceiverInstance.address, + value: 100, + }); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const balanceBefore = await Helpers.getBalance(evmRecipientAddress); + await DefaultMessageReceiverInstance.handleSygmaMessage(ZERO_ADDRESS, 0, message, { + from: handlerAddress, + gas: 200000, + value: 100, + }); + const balanceAfter = await Helpers.getBalance(evmRecipientAddress); + assert.equal(balanceAfter, balanceBefore + 200n); + assert.equal(await Helpers.getBalance(DefaultMessageReceiverInstance.address), 0n); + }); + + it("should return full original token sent balance", async () => { + const actions = []; + await ERC20MintableInstance.mint(DefaultMessageReceiverInstance.address, 333); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + await DefaultMessageReceiverInstance.handleSygmaMessage( + ERC20MintableInstance.address, + 333, + message, + { + from: handlerAddress, + gas: 200000, + } + ); + const balanceAfter = await Helpers.getTokenBalance(ERC20MintableInstance, evmRecipientAddress); + assert.equal(balanceAfter, 333n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, DefaultMessageReceiverInstance.address), 0n); + }); + + it("should return full native token balance if contract balance increased during handling and actions reverted", + async () => { + const actions = [{ + nativeValue: 100, + callTo: TestForwarderInstance.address, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ZERO_ADDRESS, + data: "0x", + }]; + await web3.eth.sendTransaction({ + from: adminAddress, + to: DefaultMessageReceiverInstance.address, + value: 100, + }); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const balanceBefore = await Helpers.getBalance(evmRecipientAddress); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage(ZERO_ADDRESS, 0, message, { + from: handlerAddress, + gas: 200000, + value: 100, + }); + const balanceAfter = await Helpers.getBalance(evmRecipientAddress); + assert.equal(balanceAfter, balanceBefore + 200n); + assert.equal(await Helpers.getBalance(DefaultMessageReceiverInstance.address), 0n); + TruffleAssert.eventEmitted(tx, "TransferRecovered", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ZERO_ADDRESS && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 0 + ); + }); + }); + + it("should return full original token sent balance if actions reverted", async () => { + const actions = [{ + nativeValue: 0, + callTo: TestForwarderInstance.address, + approveTo: ZERO_ADDRESS, + tokenSend: ERC20MintableInstance.address, + tokenReceive: ZERO_ADDRESS, + data: "0x", + }]; + await ERC20MintableInstance.mint(DefaultMessageReceiverInstance.address, 333); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ERC20MintableInstance.address, + 333, + message, + { + from: handlerAddress, + gas: 200000, + } + ); + const balanceAfter = await Helpers.getTokenBalance(ERC20MintableInstance, evmRecipientAddress); + assert.equal(balanceAfter, 333n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, DefaultMessageReceiverInstance.address), 0n); + TruffleAssert.eventEmitted(tx, "TransferRecovered", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ERC20MintableInstance.address && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 333 + ); + }); + }); + + it("should return action tokens leftovers", async () => { + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ERC20MintableInstance.address, + data: (await ERC20MintableInstance.transfer.request(adminAddress, 33)).data, + }]; + await ERC20MintableInstance.mint(DefaultMessageReceiverInstance.address, 333); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ZERO_ADDRESS, + 0, + message, + { + from: handlerAddress, + gas: 200000, + } + ); + const balanceAfter = await Helpers.getTokenBalance(ERC20MintableInstance, evmRecipientAddress); + assert.equal(balanceAfter, 300n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, DefaultMessageReceiverInstance.address), 0n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, adminAddress), 33n); + TruffleAssert.eventEmitted(tx, "Executed", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ZERO_ADDRESS && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 0 + ); + }); + }); + + it("should give approval to the approveTo then revoke it", async () => { + // DMR -> TestForwarder.execute -> TestForwarder2.execute -> Token.transferFrom(DMR, admin) + const transferFrom = (await ERC20MintableInstance.transferFrom.request( + DefaultMessageReceiverInstance.address, adminAddress, 33) + ).data; + const transferFromExecute = (await TestForwarderInstance2.execute.request( + transferFrom, ERC20MintableInstance.address, ZERO_ADDRESS) + ).data; + const actions = [{ + nativeValue: 0, + callTo: TestForwarderInstance.address, + approveTo: TestForwarderInstance2.address, + tokenSend: ERC20MintableInstance.address, + tokenReceive: ZERO_ADDRESS, + data: (await TestForwarderInstance.execute.request( + transferFromExecute, TestForwarderInstance2.address, ZERO_ADDRESS) + ).data, + }]; + await ERC20MintableInstance.mint(DefaultMessageReceiverInstance.address, 333); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ERC20MintableInstance.address, + 333, + message, + { + from: handlerAddress, + gas: 500000, + } + ); + const balanceAfter = await Helpers.getTokenBalance(ERC20MintableInstance, evmRecipientAddress); + assert.equal(balanceAfter, 300n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, DefaultMessageReceiverInstance.address), 0n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, adminAddress), 33n); + TruffleAssert.eventEmitted(tx, "Executed", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ERC20MintableInstance.address && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 333 + ); + }); + assert.equal(await ERC20MintableInstance.allowance( + DefaultMessageReceiverInstance.address, TestForwarderInstance2.address), + 0n); + assert.equal(await ERC20MintableInstance.allowance( + DefaultMessageReceiverInstance.address, TestForwarderInstance.address), + 0n); + }); + + it("should revert if callTo is EOA and data is not empty", async () => { + const actions = [{ + nativeValue: 0, + callTo: adminAddress, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ZERO_ADDRESS, + data: "0x11", + }]; + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ZERO_ADDRESS, + 0, + message, + { + from: handlerAddress, + gas: 200000, + } + ); + TruffleAssert.eventEmitted(tx, "TransferRecovered", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ZERO_ADDRESS && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 0 + ); + }); + }); + + it("should succeed if callTo is EOA and data is empty", async () => { + const actions = [{ + nativeValue: 0, + callTo: adminAddress, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ZERO_ADDRESS, + data: "0x", + }]; + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ZERO_ADDRESS, + 0, + message, + { + from: handlerAddress, + gas: 200000, + } + ); + TruffleAssert.eventEmitted(tx, "Executed", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ZERO_ADDRESS && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 0 + ); + }); + }); + + it("should send native token as part of the action", async () => { + const actions = [{ + nativeValue: 100, + callTo: relayer1Address, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ZERO_ADDRESS, + data: "0x", + }]; + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + const balanceBefore = await Helpers.getBalance(relayer1Address); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ZERO_ADDRESS, + 0, + message, + { + from: handlerAddress, + gas: 200000, + value: 300, + } + ); + const balanceAfter = await Helpers.getBalance(relayer1Address); + assert.equal(balanceAfter, balanceBefore + 100n); + assert.equal(await Helpers.getBalance(DefaultMessageReceiverInstance.address), 0n); + TruffleAssert.eventEmitted(tx, "Executed", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ZERO_ADDRESS && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 0 + ); + }); + }); + + it("should revert if has too little gas after actions", async () => { + // DMR -> TestForwarder.execute -> TestForwarder2.execute -> Token.transferFrom(DMR, admin) + const transferFrom = (await ERC20MintableInstance.transferFrom.request( + DefaultMessageReceiverInstance.address, adminAddress, 33) + ).data; + const transferFromExecute = (await TestForwarderInstance2.execute.request( + transferFrom, ERC20MintableInstance.address, ZERO_ADDRESS) + ).data; + const actions = [{ + nativeValue: 0, + callTo: TestForwarderInstance.address, + approveTo: TestForwarderInstance2.address, + tokenSend: ERC20MintableInstance.address, + tokenReceive: ZERO_ADDRESS, + data: (await TestForwarderInstance.execute.request( + transferFromExecute, TestForwarderInstance2.address, ZERO_ADDRESS) + ).data, + }]; + await ERC20MintableInstance.mint(DefaultMessageReceiverInstance.address, 333); + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + await Helpers.expectToRevertWithCustomError( + DefaultMessageReceiverInstance.handleSygmaMessage.call( + ERC20MintableInstance.address, + 333, + message, + { + from: handlerAddress, + gas: 200000, + } + ), + "InsufficientGasLimit()" + ); + }); + + it("should execute transferBalanceAction", async () => { + const actions = [{ + nativeValue: 0, + callTo: DefaultMessageReceiverInstance.address, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ZERO_ADDRESS, + data: (await DefaultMessageReceiverInstance.transferBalanceAction.request( + ZERO_ADDRESS, relayer1Address) + ).data, + }, { + nativeValue: 0, + callTo: DefaultMessageReceiverInstance.address, + approveTo: ZERO_ADDRESS, + tokenSend: ZERO_ADDRESS, + tokenReceive: ZERO_ADDRESS, + data: (await DefaultMessageReceiverInstance.transferBalanceAction.request( + ERC20MintableInstance.address, relayer1Address) + ).data, + }]; + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + await ERC20MintableInstance.mint(DefaultMessageReceiverInstance.address, 333); + const balanceBefore = await Helpers.getBalance(relayer1Address); + const tx = await DefaultMessageReceiverInstance.handleSygmaMessage( + ZERO_ADDRESS, + 0, + message, + { + from: handlerAddress, + gas: 200000, + value: 300, + } + ); + const balanceAfter = await Helpers.getBalance(relayer1Address); + assert.equal(balanceAfter, balanceBefore + 300n); + assert.equal(await Helpers.getBalance(DefaultMessageReceiverInstance.address), 0n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, DefaultMessageReceiverInstance.address), 0n); + assert.equal(await Helpers.getTokenBalance(ERC20MintableInstance, relayer1Address), 333n); + TruffleAssert.eventEmitted(tx, "Executed", (event) => { + return ( + event.transactionId === transactionId && + event.tokenSend === ZERO_ADDRESS && + event.receiver === evmRecipientAddress && + event.amount.toNumber() === 0 + ); + }); + }); +}); diff --git a/test/defaultMessageReceiver/executeProposal.js b/test/defaultMessageReceiver/executeProposal.js new file mode 100644 index 00000000..5d73c9ab --- /dev/null +++ b/test/defaultMessageReceiver/executeProposal.js @@ -0,0 +1,263 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); + +contract("Bridge - [execute proposal - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const depositAmount = Ethers.utils.parseEther("1"); + const fee = Ethers.utils.parseEther("0.1"); + const transferredAmount = depositAmount.sub(fee); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let ERC20MintableInstance; + let proposal; + let depositProposalData; + let message; + + 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 + ); + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + DefaultMessageReceiverInstance.address, + ); + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + NativeTokenHandlerInstance.address + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + Ethers.constants.AddressZero, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [evmRecipientAddress, "20"]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("should revert if handler does not have SYGMA_HANDLER_ROLE", async () => { + await DefaultMessageReceiverInstance.revokeRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + NativeTokenHandlerInstance.address + ); + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + executionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + const executeTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + TruffleAssert.eventEmitted(executeTx, "FailedHandlerExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.lowLevelData === "0xdeda9030" // InsufficientPermission() + ); + }); + }); + + it("should revert if insufficient gas limit left for executing action", async () => { + const insufficientExecutionGasAmount = 100000; + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.depositToEVMWithMessage( + originDomainID, + Ethers.constants.AddressZero, + insufficientExecutionGasAmount, + message, + { + from: depositorAddress, + value: depositAmount + }) + ); + + const executeTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: insufficientExecutionGasAmount + } + ); + + TruffleAssert.eventEmitted(executeTx, "FailedHandlerExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.lowLevelData === "0x60ee1247" // InsufficientGasLimit() + ); + }); + }); + + it("should fail to transfer funds if invalid message is provided", async () => { + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: Ethers.constants.AddressZero, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [evmRecipientAddress, "20"]), + }] + const message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + const depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + const proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + const executeTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + TruffleAssert.eventEmitted(executeTx, "FailedHandlerExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.lowLevelData === "0x2ed7fc0e" // FailedFundsTransfer() + ); + }); + }); +}); diff --git a/test/e2e/erc20/decimals/bothChainsNot18Decimals.js b/test/e2e/erc20/decimals/bothChainsNot18Decimals.js index 532a8053..8d9f810d 100644 --- a/test/e2e/erc20/decimals/bothChainsNot18Decimals.js +++ b/test/e2e/erc20/decimals/bothChainsNot18Decimals.js @@ -7,6 +7,7 @@ const TruffleAssert = require("truffle-assertions"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauserDecimals"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("E2E ERC20 - Two EVM Chains both with decimal places != 18", async accounts => { @@ -32,6 +33,7 @@ contract("E2E ERC20 - Two EVM Chains both with decimal places != 18", async acco let OriginBridgeInstance; let OriginERC20MintableInstance; + let OriginDefaultMessageReceiverInstance; let OriginERC20HandlerInstance; let originDepositData; let originResourceID; @@ -40,6 +42,7 @@ contract("E2E ERC20 - Two EVM Chains both with decimal places != 18", async acco let DestinationBridgeInstance; let DestinationERC20MintableInstance; + let DestinationDefaultMessageReceiverInstance; let DestinationERC20HandlerInstance; let destinationDepositData; let destinationResourceID; @@ -66,11 +69,23 @@ contract("E2E ERC20 - Two EVM Chains both with decimal places != 18", async acco destinationInitialContractAddresses = [DestinationERC20MintableInstance.address]; destinationBurnableContractAddresses = [DestinationERC20MintableInstance.address]; + OriginDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + DestinationDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); await Promise.all([ - ERC20HandlerContract.new(OriginBridgeInstance.address) - .then(instance => OriginERC20HandlerInstance = instance), - ERC20HandlerContract.new(DestinationBridgeInstance.address) - .then(instance => DestinationERC20HandlerInstance = instance), + ERC20HandlerContract.new( + OriginBridgeInstance.address, + OriginDefaultMessageReceiverInstance.address + ).then(instance => OriginERC20HandlerInstance = instance), + ERC20HandlerContract.new( + DestinationBridgeInstance.address, + DestinationDefaultMessageReceiverInstance.address + ).then(instance => DestinationERC20HandlerInstance = instance), ]); await OriginERC20MintableInstance.mint(depositorAddress, initialTokenAmount); diff --git a/test/e2e/erc20/decimals/oneChainNot18Decimals.js b/test/e2e/erc20/decimals/oneChainNot18Decimals.js index 19a184c0..a7d32e19 100644 --- a/test/e2e/erc20/decimals/oneChainNot18Decimals.js +++ b/test/e2e/erc20/decimals/oneChainNot18Decimals.js @@ -7,6 +7,7 @@ const TruffleAssert = require("truffle-assertions"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauserDecimals"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with != 18", async accounts => { @@ -34,6 +35,7 @@ contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with let OriginBridgeInstance; let OriginERC20MintableInstance; + let OriginDefaultMessageReceiverInstance; let OriginERC20HandlerInstance; let originDepositData; let originResourceID; @@ -42,6 +44,7 @@ contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with let DestinationBridgeInstance; let DestinationERC20MintableInstance; + let DestinationDefaultMessageReceiverInstance; let DestinationERC20HandlerInstance; let destinationDepositData; let destinationDepositProposalData; @@ -71,11 +74,23 @@ contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with destinationInitialContractAddresses = [DestinationERC20MintableInstance.address]; destinationBurnableContractAddresses = [DestinationERC20MintableInstance.address]; + OriginDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + DestinationDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); await Promise.all([ - ERC20HandlerContract.new(OriginBridgeInstance.address) - .then(instance => OriginERC20HandlerInstance = instance), - ERC20HandlerContract.new(DestinationBridgeInstance.address) - .then(instance => DestinationERC20HandlerInstance = instance), + ERC20HandlerContract.new( + OriginBridgeInstance.address, + OriginDefaultMessageReceiverInstance.address + ).then(instance => OriginERC20HandlerInstance = instance), + ERC20HandlerContract.new( + DestinationBridgeInstance.address, + DestinationDefaultMessageReceiverInstance.address + ).then(instance => DestinationERC20HandlerInstance = instance), ]); await OriginERC20MintableInstance.mint(depositorAddress, initialTokenAmount); diff --git a/test/e2e/erc20/decimals/oneChainWith0Decimals.js b/test/e2e/erc20/decimals/oneChainWith0Decimals.js index fa134743..7cfa7287 100644 --- a/test/e2e/erc20/decimals/oneChainWith0Decimals.js +++ b/test/e2e/erc20/decimals/oneChainWith0Decimals.js @@ -7,6 +7,7 @@ const TruffleAssert = require("truffle-assertions"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauserDecimals"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with == 0", async accounts => { @@ -34,6 +35,7 @@ contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with let OriginBridgeInstance; let OriginERC20MintableInstance; + let OriginDefaultMessageReceiverInstance; let OriginERC20HandlerInstance; let originDepositData; let originResourceID; @@ -42,6 +44,7 @@ contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with let DestinationBridgeInstance; let DestinationERC20MintableInstance; + let DestinationDefaultMessageReceiverInstance; let DestinationERC20HandlerInstance; let destinationDepositData; let destinationDepositProposalData; @@ -71,11 +74,23 @@ contract("E2E ERC20 - Two EVM Chains, one with decimal places == 18, other with destinationInitialContractAddresses = [DestinationERC20MintableInstance.address]; destinationBurnableContractAddresses = [DestinationERC20MintableInstance.address]; + OriginDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + DestinationDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); await Promise.all([ - ERC20HandlerContract.new(OriginBridgeInstance.address) - .then(instance => OriginERC20HandlerInstance = instance), - ERC20HandlerContract.new(DestinationBridgeInstance.address) - .then(instance => DestinationERC20HandlerInstance = instance), + ERC20HandlerContract.new( + OriginBridgeInstance.address, + OriginDefaultMessageReceiverInstance.address + ).then(instance => OriginERC20HandlerInstance = instance), + ERC20HandlerContract.new( + DestinationBridgeInstance.address, + DestinationDefaultMessageReceiverInstance.address + ).then(instance => DestinationERC20HandlerInstance = instance), ]); await OriginERC20MintableInstance.mint(depositorAddress, initialTokenAmount); diff --git a/test/e2e/erc20/decimals/roundingLoss.js b/test/e2e/erc20/decimals/roundingLoss.js index d8db7919..3ee5f900 100644 --- a/test/e2e/erc20/decimals/roundingLoss.js +++ b/test/e2e/erc20/decimals/roundingLoss.js @@ -7,6 +7,7 @@ const TruffleAssert = require("truffle-assertions"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauserDecimals"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("E2E ERC20 - Two EVM Chains both with decimal places != 18 with rounding loss", async accounts => { @@ -34,6 +35,7 @@ contract("E2E ERC20 - Two EVM Chains both with decimal places != 18 with roundin let OriginBridgeInstance; let OriginERC20MintableInstance; + let OriginDefaultMessageReceiverInstance; let OriginERC20HandlerInstance; let originDepositData; let originResourceID; @@ -42,6 +44,7 @@ contract("E2E ERC20 - Two EVM Chains both with decimal places != 18 with roundin let DestinationBridgeInstance; let DestinationERC20MintableInstance; + let DestinationDefaultMessageReceiverInstance; let DestinationERC20HandlerInstance; let destinationDepositData; let destinationResourceID; @@ -68,11 +71,23 @@ contract("E2E ERC20 - Two EVM Chains both with decimal places != 18 with roundin destinationInitialContractAddresses = [DestinationERC20MintableInstance.address]; destinationBurnableContractAddresses = [DestinationERC20MintableInstance.address]; + OriginDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + DestinationDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); await Promise.all([ - ERC20HandlerContract.new(OriginBridgeInstance.address) - .then(instance => OriginERC20HandlerInstance = instance), - ERC20HandlerContract.new(DestinationBridgeInstance.address) - .then(instance => DestinationERC20HandlerInstance = instance), + ERC20HandlerContract.new( + OriginBridgeInstance.address, + OriginDefaultMessageReceiverInstance.address + ).then(instance => OriginERC20HandlerInstance = instance), + ERC20HandlerContract.new( + DestinationBridgeInstance.address, + DestinationDefaultMessageReceiverInstance.address + ).then(instance => DestinationERC20HandlerInstance = instance), ]); await OriginERC20MintableInstance.mint(depositorAddress, initialTokenAmount); diff --git a/test/e2e/erc20/differentChainsMock.js b/test/e2e/erc20/differentChainsMock.js index abbdc6c6..78be0398 100644 --- a/test/e2e/erc20/differentChainsMock.js +++ b/test/e2e/erc20/differentChainsMock.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("E2E ERC20 - Two EVM Chains", async (accounts) => { @@ -28,6 +29,7 @@ contract("E2E ERC20 - Two EVM Chains", async (accounts) => { let OriginBridgeInstance; let OriginERC20MintableInstance; + let OriginDefaultMessageReceiverInstance; let OriginERC20HandlerInstance; let originDepositData; let originDepositProposalData; @@ -36,6 +38,7 @@ contract("E2E ERC20 - Two EVM Chains", async (accounts) => { let DestinationBridgeInstance; let DestinationERC20MintableInstance; + let DestinationDefaultMessageReceiverInstance; let DestinationERC20HandlerInstance; let destinationDepositData; let destinationDepositProposalData; @@ -83,11 +86,25 @@ contract("E2E ERC20 - Two EVM Chains", async (accounts) => { DestinationERC20MintableInstance.address, ]; + OriginDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); + DestinationDefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); await Promise.all([ - ERC20HandlerContract.new(OriginBridgeInstance.address).then( + ERC20HandlerContract.new( + OriginBridgeInstance.address, + OriginDefaultMessageReceiverInstance.address + ).then( (instance) => (OriginERC20HandlerInstance = instance) ), - ERC20HandlerContract.new(DestinationBridgeInstance.address).then( + ERC20HandlerContract.new( + DestinationBridgeInstance.address, + DestinationDefaultMessageReceiverInstance.address + ).then( (instance) => (DestinationERC20HandlerInstance = instance) ), ]); diff --git a/test/e2e/erc20/sameChain.js b/test/e2e/erc20/sameChain.js index 5e3dfc91..a5b5cfa3 100644 --- a/test/e2e/erc20/sameChain.js +++ b/test/e2e/erc20/sameChain.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("E2E ERC20 - Same Chain", async (accounts) => { @@ -27,6 +28,7 @@ contract("E2E ERC20 - Same Chain", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; let ERC20HandlerInstance; + let DefaultMessageReceiverInstance; let resourceID; let depositData; @@ -50,8 +52,13 @@ contract("E2E ERC20 - Same Chain", async (accounts) => { originDomainID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new( + [], + 100000 + ); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await Promise.all([ diff --git a/test/gasBenchmarks/deployments.js b/test/gasBenchmarks/deployments.js index 76b46c57..24bd17c2 100644 --- a/test/gasBenchmarks/deployments.js +++ b/test/gasBenchmarks/deployments.js @@ -7,6 +7,7 @@ const BridgeContract = artifacts.require("Bridge"); const AccessControlSegregatorContract = artifacts.require( "AccessControlSegregator" ); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC721HandlerContract = artifacts.require("ERC721Handler"); const ERC1155HandlerContract = artifacts.require("ERC1155Handler"); @@ -25,6 +26,7 @@ contract("Gas Benchmark - [contract deployments]", async (accounts) => { const gasBenchmarks = []; let BridgeInstance; + let DefaultMessageReceiverInstance; it.skip("Should deploy all contracts and print benchmarks", async () => { const accessControlInstance = await AccessControlSegregatorContract.new( @@ -37,7 +39,10 @@ contract("Gas Benchmark - [contract deployments]", async (accounts) => { await BridgeContract.new(domainID, accessControlInstance.address).then( (instance) => (BridgeInstance = instance) ), - ERC20HandlerContract.new(BridgeInstance.address), + await DefaultMessageReceiverContract.new([], 100000).then( + (instance) => (DefaultMessageReceiverInstance = instance) + ), + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address), ERC721HandlerContract.new(BridgeInstance.address), ERC1155HandlerContract.new(BridgeInstance.address), GmpHandlerContract.new(BridgeInstance.address), diff --git a/test/gasBenchmarks/deposits.js b/test/gasBenchmarks/deposits.js index a3c51929..4aef8939 100644 --- a/test/gasBenchmarks/deposits.js +++ b/test/gasBenchmarks/deposits.js @@ -1,5 +1,6 @@ // The Licensed Work is (c) 2022 Sygma // SPDX-License-Identifier: LGPL-3.0-only +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); const ERC721HandlerContract = artifacts.require("ERC721Handler"); @@ -28,6 +29,7 @@ contract("Gas Benchmark - [Deposits]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let ERC721MintableInstance; let ERC721HandlerInstance; @@ -68,8 +70,10 @@ contract("Gas Benchmark - [Deposits]", async (accounts) => { originDomainID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + await Promise.all([ - ERC20HandlerContract.new(BridgeInstance.address).then( + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address).then( (instance) => (ERC20HandlerInstance = instance) ), ERC20MintableInstance.mint(depositorAddress, erc20TokenAmount), diff --git a/test/gasBenchmarks/executeProposal.js b/test/gasBenchmarks/executeProposal.js index d2d07110..10f9b9c9 100644 --- a/test/gasBenchmarks/executeProposal.js +++ b/test/gasBenchmarks/executeProposal.js @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only const Helpers = require("../helpers"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); const ERC721HandlerContract = artifacts.require("ERC721Handler"); @@ -28,6 +29,7 @@ contract("Gas Benchmark - [Execute Proposal]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let ERC721MintableInstance; let ERC721HandlerInstance; @@ -90,8 +92,10 @@ contract("Gas Benchmark - [Execute Proposal]", async (accounts) => { originDomainID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + await Promise.all([ - ERC20HandlerContract.new(BridgeInstance.address).then( + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address).then( (instance) => (ERC20HandlerInstance = instance) ), ERC20MintableInstance.mint(depositorAddress, erc20TokenAmount), diff --git a/test/handlers/erc20/constructor.js b/test/handlers/erc20/constructor.js index 66b754a5..ac80adb6 100644 --- a/test/handlers/erc20/constructor.js +++ b/test/handlers/erc20/constructor.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("ERC20Handler - [constructor]", async (accounts) => { @@ -14,6 +15,7 @@ contract("ERC20Handler - [constructor]", async (accounts) => { const emptySetResourceData = "0x"; let BridgeInstance; + let DefaultMessageReceiverInstance; let ERC20MintableInstance1; let ERC20MintableInstance2; let ERC20MintableInstance3; @@ -21,6 +23,8 @@ contract("ERC20Handler - [constructor]", async (accounts) => { let initialContractAddresses; beforeEach(async () => { + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + await Promise.all([ (BridgeInstance = await Helpers.deployBridge(domainID, accounts[0])), ERC20MintableContract.new("token", "TOK").then( @@ -68,7 +72,7 @@ contract("ERC20Handler - [constructor]", async (accounts) => { it("[sanity] contract should be deployed successfully", async () => { await TruffleAssert.passes( - ERC20HandlerContract.new(BridgeInstance.address) + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address) ); }); @@ -84,7 +88,8 @@ contract("ERC20Handler - [constructor]", async (accounts) => { "initialResourceIDs should be parsed correctly and corresponding resourceID mappings should have expected values", async () => { const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); for (i = 0; i < initialResourceIDs.length; i++) { diff --git a/test/handlers/erc20/decimals.js b/test/handlers/erc20/decimals.js index b5f2dd97..cc8c79e4 100644 --- a/test/handlers/erc20/decimals.js +++ b/test/handlers/erc20/decimals.js @@ -5,6 +5,7 @@ const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauserDecimals"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("ERC20Handler - [decimals]", async (accounts) => { @@ -23,6 +24,7 @@ contract("ERC20Handler - [decimals]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let resourceID; @@ -47,8 +49,10 @@ contract("ERC20Handler - [decimals]", async (accounts) => { depositProposalData = Helpers.createERCDepositData(depositAmount, 20, recipientAddress) + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); await Promise.all([ - ERC20HandlerContract.new(BridgeInstance.address).then(instance => ERC20HandlerInstance = instance), + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address) + .then(instance => ERC20HandlerInstance = instance), ERC20MintableInstance.mint(depositorAddress, tokenAmount) ]); diff --git a/test/handlers/erc20/deposit.js b/test/handlers/erc20/deposit.js index 875a47b6..d89ac045 100644 --- a/test/handlers/erc20/deposit.js +++ b/test/handlers/erc20/deposit.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { @@ -20,6 +21,7 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let resourceID; @@ -43,8 +45,9 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { initialContractAddresses = [ERC20MintableInstance.address]; burnableContractAddresses = []; + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); await Promise.all([ - ERC20HandlerContract.new(BridgeInstance.address).then( + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address).then( (instance) => (ERC20HandlerInstance = instance) ), ERC20MintableInstance.mint(depositorAddress, tokenAmount), @@ -149,10 +152,10 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { it(`When non-contract addresses are whitelisted in the handler, deposits which the addresses are set as a token address will be failed`, async () => { - const ZERO_Address = "0x0000000000000000000000000000000000000000"; + const NonContract_Address = "0x0000000000000000000000000000000000001111"; const EOA_Address = accounts[1]; - const resourceID_ZERO_Address = Helpers.createResourceID( - ZERO_Address, + const resourceID_NonContract_Address = Helpers.createResourceID( + NonContract_Address, originDomainID ); const resourceID_EOA_Address = Helpers.createResourceID( @@ -161,8 +164,8 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { ); await BridgeInstance.adminSetResource( ERC20HandlerInstance.address, - resourceID_ZERO_Address, - ZERO_Address, + resourceID_NonContract_Address, + NonContract_Address, emptySetResourceData ); await BridgeInstance.adminSetResource( @@ -178,7 +181,7 @@ contract("ERC20Handler - [Deposit ERC20]", async (accounts) => { await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, - resourceID_ZERO_Address, + resourceID_NonContract_Address, Helpers.createERCDepositData( tokenAmount, lenRecipientAddress, diff --git a/test/handlers/erc20/depositBurn.js b/test/handlers/erc20/depositBurn.js index 5781724d..f3fc8da3 100644 --- a/test/handlers/erc20/depositBurn.js +++ b/test/handlers/erc20/depositBurn.js @@ -4,6 +4,7 @@ const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("ERC20Handler - [Deposit Burn ERC20]", async (accounts) => { @@ -51,8 +52,9 @@ contract("ERC20Handler - [Deposit Burn ERC20]", async (accounts) => { ]; burnableContractAddresses = [ERC20MintableInstance1.address]; + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); await Promise.all([ - ERC20HandlerContract.new(BridgeInstance.address).then( + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address).then( (instance) => (ERC20HandlerInstance = instance) ), ERC20MintableInstance1.mint(depositorAddress, initialTokenAmount), diff --git a/test/handlers/erc20/isBurnable.js b/test/handlers/erc20/isBurnable.js index 28b65e84..46dc8844 100644 --- a/test/handlers/erc20/isBurnable.js +++ b/test/handlers/erc20/isBurnable.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("ERC20Handler - [Burn ERC20]", async (accounts) => { @@ -14,6 +15,7 @@ contract("ERC20Handler - [Burn ERC20]", async (accounts) => { const emptySetResourceData = "0x"; let BridgeInstance; + let DefaultMessageReceiverInstance; let ERC20MintableInstance1; let ERC20MintableInstance2; let resourceID1; @@ -23,6 +25,7 @@ contract("ERC20Handler - [Burn ERC20]", async (accounts) => { let burnableContractAddresses; beforeEach(async () => { + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); await Promise.all([ (BridgeInstance = await Helpers.deployBridge(domainID, accounts[0])), ERC20MintableContract.new("token", "TOK").then( @@ -51,13 +54,14 @@ contract("ERC20Handler - [Burn ERC20]", async (accounts) => { it("[sanity] contract should be deployed successfully", async () => { await TruffleAssert.passes( - ERC20HandlerContract.new(BridgeInstance.address) + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address) ); }); it("burnableContractAddresses should be marked as burnable", async () => { const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); for (i = 0; i < initialResourceIDs.length; i++) { @@ -91,7 +95,8 @@ contract("ERC20Handler - [Burn ERC20]", async (accounts) => { it("ERC20MintableInstance2.address should not be marked as burnable", async () => { const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); for (i = 0; i < initialResourceIDs.length; i++) { @@ -123,7 +128,8 @@ contract("ERC20Handler - [Burn ERC20]", async (accounts) => { it("ERC20MintableInstance2.address should be marked as burnable after setBurnable is called", async () => { const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); for (i = 0; i < initialResourceIDs.length; i++) { @@ -160,7 +166,8 @@ contract("ERC20Handler - [Burn ERC20]", async (accounts) => { it(`ERC20MintableInstances should not be marked as burnable after setResource is called on already burnable tokens`, async () => { const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); for (i = 0; i < initialResourceIDs.length; i++) { diff --git a/test/handlers/erc20/isWhitelisted.js b/test/handlers/erc20/isWhitelisted.js index da4d4d0d..a83aafa7 100644 --- a/test/handlers/erc20/isWhitelisted.js +++ b/test/handlers/erc20/isWhitelisted.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract("ERC20Handler - [isWhitelisted]", async (accounts) => { @@ -14,10 +15,12 @@ contract("ERC20Handler - [isWhitelisted]", async (accounts) => { const emptySetResourceData = "0x"; let BridgeInstance; + let DefaultMessageReceiverInstance; let ERC20MintableInstance1; let initialResourceIDs; beforeEach(async () => { + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); await Promise.all([ (BridgeInstance = await Helpers.deployBridge(domainID, accounts[0])), ERC20MintableContract.new("token", "TOK").then( @@ -40,13 +43,14 @@ contract("ERC20Handler - [isWhitelisted]", async (accounts) => { it("[sanity] contract should be deployed successfully", async () => { await TruffleAssert.passes( - ERC20HandlerContract.new(BridgeInstance.address) + ERC20HandlerContract.new(BridgeInstance.address, DefaultMessageReceiverInstance.address) ); }); it("initialContractAddress should be whitelisted", async () => { const ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await BridgeInstance.adminSetResource( ERC20HandlerInstance.address, diff --git a/test/handlers/erc20/optionalContracCall/collectFee.js b/test/handlers/erc20/optionalContracCall/collectFee.js new file mode 100644 index 00000000..45d65e8f --- /dev/null +++ b/test/handlers/erc20/optionalContracCall/collectFee.js @@ -0,0 +1,159 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); + +contract("Bridge - [collect fee - erc20 token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const initialTokenAmount = 100; + const depositAmount = 10; + const fee = 100000; // BPS + const feeAmount = 1; + const executionGasAmount = 30000000; + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const feeData = "0x"; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let ERC20HandlerInstance; + let PercentageFeeHandlerInstance; + let FeeHandlerRouterInstance; + let ERC721MintableInstance; + let message; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + PercentageFeeHandlerInstance = await PercentageFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address, + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token20", + "TOK20" + ); + ERC721MintableInstance = await ERC721MintableContract.new("token721", "TOK721", "") + await ERC20MintableInstance.mint(depositorAddress, initialTokenAmount); + + await BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID, + ERC20MintableInstance.address, + emptySetResourceData + ); + await PercentageFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + PercentageFeeHandlerInstance.address + ), + + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + ERC20HandlerInstance.address + ); + + await ERC20MintableInstance.approve( + ERC20HandlerInstance.address, + depositAmount, + {from: depositorAddress} + ); + + const mintableERC721Iface = new Ethers.utils.Interface( + ["function mint(address to, uint256 tokenId, string memory _data)"] + ); + const actions = [{ + nativeValue: 0, + callTo: ERC721MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC721Iface.encodeFunctionData("mint", [evmRecipientAddress, "5", ""]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + depositAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("ERC20 token transfer fee should be successfully deducted", async () => { + const depositorBalanceBefore = await ERC20MintableInstance.balanceOf(depositorAddress); + const handlerBalanceBefore = await ERC20MintableInstance.balanceOf(ERC20HandlerInstance.address); + + await TruffleAssert.passes( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + message, + feeData, + { + from: depositorAddress, + } + )); + + // check that correct ERC20 token amount is successfully transferred to the handler + const handlerBalanceAfter = await ERC20MintableInstance.balanceOf(ERC20HandlerInstance.address); + assert.strictEqual( + new Ethers.BigNumber.from(feeAmount).add(Number(handlerBalanceBefore)).toString(), + handlerBalanceAfter.toString() + ); + + // check that depositor before and after balances align + const depositorBalanceAfter = await ERC20MintableInstance.balanceOf(depositorAddress); + assert.strictEqual( + new Ethers.BigNumber.from(Number(depositorBalanceBefore) + ).sub(feeAmount).toString(), depositorBalanceAfter.toString() + ) + }); +}); diff --git a/test/handlers/erc20/optionalContracCall/decimalConversion.js b/test/handlers/erc20/optionalContracCall/decimalConversion.js new file mode 100644 index 00000000..3a8b05e9 --- /dev/null +++ b/test/handlers/erc20/optionalContracCall/decimalConversion.js @@ -0,0 +1,277 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); + + +contract("Bridge - [decimal conversion - erc20 token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + const returnBytesLength = 128; + + const expectedDepositNonce = 1; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const originDecimalPlaces = 8; + const bridgeDefaultDecimalPlaces = 18; + const initialTokenAmount = Ethers.utils.parseUnits("100", originDecimalPlaces); + const depositAmount = Ethers.utils.parseUnits("10", originDecimalPlaces); + const fee = 100000; // BPS + const feeAmount = Ethers.utils.parseUnits("1", originDecimalPlaces); + const convertedTransferAmount = Ethers.utils.parseUnits("10", bridgeDefaultDecimalPlaces); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + const amountToMint = 1; + const feeData = "0x"; + + const AbiCoder = new Ethers.utils.AbiCoder(); + const expectedHandlerResponse = AbiCoder.encode( + ["uint256"], + [convertedTransferAmount] + ); + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let ERC20HandlerInstance; + let PercentageFeeHandlerInstance; + let FeeHandlerRouterInstance; + let depositProposalData; + let ERC20MintableInstance; + let ERC721MintableInstance; + + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + PercentageFeeHandlerInstance = await PercentageFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address, + ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + ERC721MintableInstance = await ERC721MintableContract.new("token721", "TOK721", "") + await ERC20MintableInstance.mint(depositorAddress, initialTokenAmount); + await ERC20MintableInstance.mint(ERC20HandlerInstance.address, initialTokenAmount); + + await BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID, + ERC20MintableInstance.address, + originDecimalPlaces + ); + await BridgeInstance.adminSetBurnable( + ERC20HandlerInstance.address, + ERC20MintableInstance.address + ); + await PercentageFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + PercentageFeeHandlerInstance.address + ); + + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + ERC20HandlerInstance.address + ); + + await ERC721MintableInstance.grantRole( + await ERC721MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + ERC20HandlerInstance.address + ); + + await ERC20MintableInstance.approve( + PercentageFeeHandlerInstance.address, + feeAmount, + {from: depositorAddress} + ); + await ERC20MintableInstance.approve( + ERC20HandlerInstance.address, + depositAmount, + {from: depositorAddress} + ); + + const mintableERC721Iface = new Ethers.utils.Interface( + ["function mint(address to, uint256 tokenId, string memory _data)"] + ); + const actions = [{ + nativeValue: 0, + callTo: ERC721MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC721Iface.encodeFunctionData("mint", [evmRecipientAddress, "5", ""]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + depositAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + + it("[sanity] decimals value is set if args are provided to 'adminSetResource'", async () => { + const ERC20Decimals = (await ERC20HandlerInstance._tokenContractAddressToTokenProperties.call( + ERC20MintableInstance.address + )).decimals; + + assert.strictEqual(ERC20Decimals.isSet, true); + assert.strictEqual(ERC20Decimals["externalDecimals"], "8"); + }); + + it("Deposit converts sent token amount with 8 decimals to 18 decimal places", async () => { + const depositTx = await BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ); + + await TruffleAssert.passes(depositTx); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === depositorAddress && + event.data === depositProposalData && + event.handlerResponse === expectedHandlerResponse + ); + }); + }); + + it("Proposal execution converts sent token amount with 18 decimals to 8 decimal places", async () => { + const proposalData = Helpers.createOptionalContractCallDepositData( + convertedTransferAmount, // 18 decimals + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + const dataHash = Ethers.utils.keccak256( + ERC20HandlerInstance.address + proposalData.substr(2) + ); + + const proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: proposalData, + }; + + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + const recipientBalanceBefore = await ERC721MintableInstance.balanceOf(evmRecipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint16", "uint256"], + [ + ERC20MintableInstance.address, + DefaultMessageReceiverInstance.address, + convertedTransferAmount, + returnBytesLength, + 0 + ] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that ERC721 token is transferred to recipient address + const recipientBalanceAfter = await ERC721MintableInstance.balanceOf(evmRecipientAddress); + assert.strictEqual(new Ethers.BigNumber.from(amountToMint).add( + Number(recipientBalanceBefore)).toString(), + recipientBalanceAfter.toString() + ); + }); +}); diff --git a/test/handlers/erc20/optionalContracCall/deposit.js b/test/handlers/erc20/optionalContracCall/deposit.js new file mode 100644 index 00000000..ed73ba35 --- /dev/null +++ b/test/handlers/erc20/optionalContracCall/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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); + + +contract("Bridge - [deposit - erc20 token with contract call]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const initialTokenAmount = 100; + const depositAmount = 10; + const fee = 1; + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + const feeData = "0x"; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let ERC20HandlerInstance; + let PercentageFeeHandlerInstance; + let FeeHandlerRouterInstance; + let ERC20MintableInstance; + let ERC721MintableInstance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + PercentageFeeHandlerInstance = await PercentageFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address, + ); + + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + ERC721MintableInstance = await ERC721MintableContract.new("token721", "TOK721", "") + await ERC20MintableInstance.mint(depositorAddress, initialTokenAmount); + + await BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID, + ERC20MintableInstance.address, + emptySetResourceData + ); + + await ERC20MintableInstance.approve( + ERC20HandlerInstance.address, + depositAmount, + {from: depositorAddress} + ); + + await PercentageFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + PercentageFeeHandlerInstance.address + ), + + await ERC20MintableInstance.grantRole( + await ERC20MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + await ERC20MintableInstance.approve( + ERC20HandlerInstance.address, + depositAmount, + {from: depositorAddress} + ); + + const mintableERC721Iface = new Ethers.utils.Interface( + ["function mint(address to, uint256 tokenId, string memory _data)"] + ); + const actions = [{ + nativeValue: 0, + callTo: ERC721MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC721Iface.encodeFunctionData("mint", [evmRecipientAddress, "5", ""]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + + depositProposalData = Helpers.createOptionalContractCallDepositData( + depositAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("Native token deposit to EVM with message can be made", async () => { + await TruffleAssert.passes( + await BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ) + ); + }); + + it("_depositCounts should be increments from 0 to 1", async () => { + await BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ); + + const depositCount = await BridgeInstance._depositCounts.call( + destinationDomainID + ); + assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); + }); + + it("Deposit event is fired with expected value", async () => { + const depositTx = await BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === depositorAddress && + event.data === depositProposalData.toLowerCase() && + event.handlerResponse === null + ); + }); + }); + + it("Should revert if destination domain is current bridge domain", async () => { + await Helpers.reverts( + BridgeInstance.deposit( + originDomainID, + resourceID, + depositProposalData, + feeData, { + from: depositorAddress, + } + ) + ); + }); +}); diff --git a/test/handlers/erc20/optionalContracCall/distributeFee.js b/test/handlers/erc20/optionalContracCall/distributeFee.js new file mode 100644 index 00000000..53596f26 --- /dev/null +++ b/test/handlers/erc20/optionalContracCall/distributeFee.js @@ -0,0 +1,279 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +const TruffleAssert = require("truffle-assertions"); + +const Helpers = require("../../../helpers"); +const Ethers = require("ethers"); + +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); + +contract("PercentageFeeHandler - [distributeFee]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + + const expectedDepositNonce = 1; + const depositAmount = 100000; + const feeData = "0x"; + const emptySetResourceData = "0x"; + const feeAmount = 30; + const feeBps = 30000; // 3 BPS + const payout = Ethers.BigNumber.from("10"); + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + + let BridgeInstance; + let ERC20MintableInstance; + let DefaultMessageReceiverInstance; + let ERC20HandlerInstance; + let PercentageFeeHandlerInstance; + let FeeHandlerRouterInstance; + let ERC721MintableInstance; + + let resourceID; + let depositProposalData; + + const assertOnlyAdmin = (method, ...params) => { + return Helpers.reverts( + method(...params, {from: accounts[1]}), + "sender doesn't have admin role" + ); + }; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + accounts[0] + )), + ERC20MintableContract.new("token", "TOK").then( + (instance) => (ERC20MintableInstance = instance) + ), + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ), + PercentageFeeHandlerInstance = await PercentageFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ) + ]); + + resourceID = Helpers.createResourceID( + ERC20MintableInstance.address, + originDomainID + ); + + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address + ); + + ERC721MintableInstance = await ERC721MintableContract.new("token721", "TOK721", "") + + await Promise.all([ + BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID, + ERC20MintableInstance.address, + emptySetResourceData + ), + ERC20MintableInstance.mint(depositorAddress, depositAmount + feeAmount), + ERC20MintableInstance.approve(ERC20HandlerInstance.address, depositAmount, { + from: depositorAddress, + }), + ERC20MintableInstance.approve( + PercentageFeeHandlerInstance.address, + depositAmount, + {from: depositorAddress} + ), + BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + PercentageFeeHandlerInstance.address + ), + PercentageFeeHandlerInstance.changeFee(destinationDomainID, resourceID, feeBps) + ]); + + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + ERC20HandlerInstance.address + ); + + const mintableERC721Iface = new Ethers.utils.Interface( + ["function mint(address to, uint256 tokenId, string memory _data)"] + ); + const actions = [{ + nativeValue: 0, + callTo: ERC721MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC721Iface.encodeFunctionData("mint", [evmRecipientAddress, "5", ""]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + depositProposalData = Helpers.createOptionalContractCallDepositData( + depositAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("should distribute fees", async () => { + // check the balance is 0 + const b1Before = ( + await ERC20MintableInstance.balanceOf(accounts[3]) + ).toString(); + const b2Before = ( + await ERC20MintableInstance.balanceOf(accounts[4]) + ).toString(); + + await TruffleAssert.passes( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ) + ); + const balance = await ERC20MintableInstance.balanceOf( + PercentageFeeHandlerInstance.address + ); + assert.equal(balance, feeAmount); + + // Transfer the funds + const tx = await PercentageFeeHandlerInstance.transferERC20Fee( + resourceID, + [accounts[3], accounts[4]], + [payout, payout] + ); + TruffleAssert.eventEmitted(tx, "FeeDistributed", (event) => { + return ( + event.tokenAddress === ERC20MintableInstance.address && + event.recipient === accounts[3] && + event.amount.toString() === payout.toString() + ); + }); + TruffleAssert.eventEmitted(tx, "FeeDistributed", (event) => { + return ( + event.tokenAddress === ERC20MintableInstance.address && + event.recipient === accounts[4] && + event.amount.toString() === payout.toString() + ); + }); + b1 = await ERC20MintableInstance.balanceOf(accounts[3]); + b2 = await ERC20MintableInstance.balanceOf(accounts[4]); + assert.equal(b1.toString(), payout.add(b1Before).toString()); + assert.equal(b2.toString(), payout.add(b2Before).toString()); + }); + + it("should not distribute fees with other resourceID", async () => { + await TruffleAssert.passes( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ) + ); + const balance = await ERC20MintableInstance.balanceOf( + PercentageFeeHandlerInstance.address + ); + assert.equal(balance, feeAmount); + + const incorrectResourceID = Helpers.createResourceID( + PercentageFeeHandlerInstance.address, + originDomainID + ); + + // Transfer the funds: fails + await Helpers.reverts( + PercentageFeeHandlerInstance.transferERC20Fee( + incorrectResourceID, + [accounts[3], accounts[4]], + [payout, payout] + ) + ); + }); + + it("should require admin role to distribute fee", async () => { + await TruffleAssert.passes( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ) + ); + const balance = await ERC20MintableInstance.balanceOf( + PercentageFeeHandlerInstance.address + ); + assert.equal(balance, feeAmount); + + await assertOnlyAdmin( + PercentageFeeHandlerInstance.transferERC20Fee, + resourceID, + [accounts[3], accounts[4]], + [payout.toNumber(), payout.toNumber()] + ); + }); + + it("should revert if addrs and amounts arrays have different length", async () => { + await TruffleAssert.passes( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress, + } + ) + ); + const balance = await ERC20MintableInstance.balanceOf( + PercentageFeeHandlerInstance.address + ); + assert.equal(balance, feeAmount); + + await Helpers.reverts( + PercentageFeeHandlerInstance.transferERC20Fee( + resourceID, + [accounts[3], accounts[4]], + [payout, payout, payout] + ), + "addrs[], amounts[]: diff length" + ); + }); +}); diff --git a/test/handlers/erc20/optionalContracCall/executeProposal.js b/test/handlers/erc20/optionalContracCall/executeProposal.js new file mode 100644 index 00000000..dd61a6e9 --- /dev/null +++ b/test/handlers/erc20/optionalContracCall/executeProposal.js @@ -0,0 +1,346 @@ +// 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 DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); + +contract("Bridge - [execute proposal - erc20 token with contract call]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const evmRecipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const emptySetResourceData = "0x"; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const initialTokenAmount = 100; + const depositAmount = 10; + const fee = 1000000; // BPS + const transferredAmount = 9; + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const executionGasAmount = 30000000; + const feeData = "0x"; + const amountToMint = 1; + const returnBytesLength = 128; + + let BridgeInstance; + let DefaultMessageReceiverInstance; + let ERC20HandlerInstance; + let PercentageFeeHandlerInstance; + let FeeHandlerRouterInstance; + let ERC20MintableInstance; + let ERC721MintableInstance; + let dataHash; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + adminAddress + )), + ]); + + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + PercentageFeeHandlerInstance = await PercentageFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address, + ); + + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); + ERC721MintableInstance = await ERC721MintableContract.new("token721", "TOK721", "") + await ERC20MintableInstance.mint(depositorAddress, initialTokenAmount); + + await BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID, + ERC20MintableInstance.address, + emptySetResourceData + ); + + await ERC20MintableInstance.approve( + ERC20HandlerInstance.address, + depositAmount, + {from: depositorAddress} + ); + + await PercentageFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + // await PercentageFeeHandlerInstance.changeFeeBounds(resourceID, 2, 10) + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + PercentageFeeHandlerInstance.address + ), + await DefaultMessageReceiverInstance.grantRole( + await DefaultMessageReceiverInstance.SYGMA_HANDLER_ROLE(), + ERC20HandlerInstance.address + ); + await ERC721MintableInstance.grantRole( + await ERC721MintableInstance.MINTER_ROLE(), + DefaultMessageReceiverInstance.address + ); + + await ERC20MintableInstance.approve( + ERC20HandlerInstance.address, + depositAmount, + {from: depositorAddress} + ); + + const mintableERC721Iface = new Ethers.utils.Interface( + ["function mint(address to, uint256 tokenId, string memory _data)"] + ); + const actions = [{ + nativeValue: 0, + callTo: ERC721MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC721Iface.encodeFunctionData("mint", [evmRecipientAddress, "5", ""]), + }] + message = Helpers.createMessageCallData( + transactionId, + actions, + evmRecipientAddress + ); + + + depositProposalData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + executionGasAmount, + message + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData + }; + + dataHash = Ethers.utils.keccak256( + ERC20HandlerInstance.address + depositProposalData.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 with contract call successfully", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress + }) + ); + + const recipientNativeBalanceBefore = await web3.eth.getBalance(evmRecipientAddress); + const recipientERC721BalanceBefore = await ERC721MintableInstance.balanceOf(evmRecipientAddress); + const defaultReceiverBalanceBefore = await web3.eth.getBalance(DefaultMessageReceiverInstance.address); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + gas: executionGasAmount + }) + ); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that tokens are transferred to recipient address + const recipientNativeBalanceAfter = await web3.eth.getBalance(evmRecipientAddress); + const recipientERC721BalanceAfter = await ERC721MintableInstance.balanceOf(evmRecipientAddress); + const defaultReceiverBalanceAfter = await web3.eth.getBalance(DefaultMessageReceiverInstance.address); + + assert.strictEqual( + recipientNativeBalanceBefore, + recipientNativeBalanceAfter + ); + assert.strictEqual(new Ethers.BigNumber.from(amountToMint).add( + recipientERC721BalanceBefore.toString()).toString(), recipientERC721BalanceAfter.toString() + ); + assert.strictEqual(defaultReceiverBalanceBefore.toString(), defaultReceiverBalanceAfter.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( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress + }) + ); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + gas: executionGasAmount + }) + ); + + const skipExecuteTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + // 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( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress + }) + ); + + const recipientBalanceBefore = await ERC20MintableInstance.balanceOf(evmRecipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + { + from: relayer1Address, + gas: executionGasAmount + } + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint16", "uint256"], + [ + ERC20MintableInstance.address, + DefaultMessageReceiverInstance.address, + transferredAmount, + returnBytesLength, + 0 + ] + ) + ); + }); + + // check that deposit nonce has been marked as used in bitmap + assert.isTrue( + await BridgeInstance.isProposalExecuted( + originDomainID, + expectedDepositNonce + ) + ); + + // check that ERC20 tokens are transferred to recipient address + const recipientBalanceAfter = await ERC20MintableInstance.balanceOf(evmRecipientAddress); + assert.strictEqual(new Ethers.BigNumber.from(transferredAmount).add( + Number(recipientBalanceBefore)).toString(), + recipientBalanceAfter.toString() + ); + }); + + 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( + BridgeInstance.deposit( + destinationDomainID, + resourceID, + depositProposalData, + feeData, + { + from: depositorAddress + }) + ); + + await Helpers.expectToRevertWithCustomError( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }), + "InvalidProposalSigner()" + ); + }); +}); diff --git a/test/handlers/erc20/setResourceIDAndContractAddress.js b/test/handlers/erc20/setResourceIDAndContractAddress.js index 00645d54..a15f87f3 100644 --- a/test/handlers/erc20/setResourceIDAndContractAddress.js +++ b/test/handlers/erc20/setResourceIDAndContractAddress.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); contract( @@ -16,6 +17,7 @@ contract( let BridgeInstance; let ERC20MintableInstance1; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let initialResourceIDs; let initialContractAddresses; @@ -37,8 +39,10 @@ contract( initialContractAddresses = [ERC20MintableInstance1.address]; burnableContractAddresses = []; + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await BridgeInstance.adminSetResource( ERC20HandlerInstance.address, @@ -153,7 +157,8 @@ contract( "TOK" ); ERC20HandlerInstance2 = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await BridgeInstance.adminSetResource( diff --git a/test/handlers/fee/basic/calculateFee.js b/test/handlers/fee/basic/calculateFee.js index ff244904..1118772b 100644 --- a/test/handlers/fee/basic/calculateFee.js +++ b/test/handlers/fee/basic/calculateFee.js @@ -6,6 +6,7 @@ const Ethers = require("ethers"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); @@ -22,6 +23,7 @@ contract("BasicFeeHandler - [calculateFee]", async (accounts) => { let BasicFeeHandlerInstance; let resourceID; let depositData; + let DefaultMessageReceiverInstance; let ERC20MintableInstance; let FeeHandlerRouterInstance; @@ -36,8 +38,10 @@ contract("BasicFeeHandler - [calculateFee]", async (accounts) => { ), ]); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address diff --git a/test/handlers/fee/basic/collectFee.js b/test/handlers/fee/basic/collectFee.js index e1356fdf..5b3e6173 100644 --- a/test/handlers/fee/basic/collectFee.js +++ b/test/handlers/fee/basic/collectFee.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const ERC721MintableContract = artifacts.require("ERC721MinterBurnerPauser"); const ERC721HandlerContract = artifacts.require("ERC721Handler"); @@ -27,6 +28,7 @@ contract("BasicFeeHandler - [collectFee]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let ERC721HandlerInstance; let ERC721MintableInstance; @@ -63,8 +65,10 @@ contract("BasicFeeHandler - [collectFee]", async (accounts) => { originDomainID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); ERC721HandlerInstance = await ERC721HandlerContract.new( BridgeInstance.address diff --git a/test/handlers/fee/basic/distributeFee.js b/test/handlers/fee/basic/distributeFee.js index f74b130d..069ee784 100644 --- a/test/handlers/fee/basic/distributeFee.js +++ b/test/handlers/fee/basic/distributeFee.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); @@ -31,6 +32,7 @@ contract("BasicFeeHandler - [distributeFee]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let BasicFeeHandlerInstance; let FeeHandlerRouterInstance; @@ -57,8 +59,10 @@ contract("BasicFeeHandler - [distributeFee]", async (accounts) => { FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await Promise.all([ diff --git a/test/handlers/fee/percentage/calculateFee.js b/test/handlers/fee/percentage/calculateFee.js index b6c1b9dc..1233f4be 100644 --- a/test/handlers/fee/percentage/calculateFee.js +++ b/test/handlers/fee/percentage/calculateFee.js @@ -4,6 +4,7 @@ const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); @@ -21,6 +22,7 @@ contract("PercentageFeeHandler - [calculateFee]", async (accounts) => { let resourceID; let ERC20MintableInstance; let FeeHandlerRouterInstance; + let DefaultMessageReceiverInstance; beforeEach(async () => { await Promise.all([ @@ -33,8 +35,10 @@ contract("PercentageFeeHandler - [calculateFee]", async (accounts) => { ), ]); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address diff --git a/test/handlers/fee/percentage/collectFee.js b/test/handlers/fee/percentage/collectFee.js index 1e182748..f02c6838 100644 --- a/test/handlers/fee/percentage/collectFee.js +++ b/test/handlers/fee/percentage/collectFee.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../../../helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); @@ -32,6 +33,7 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { let depositData; let FeeHandlerRouterInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let ERC20MintableInstance; @@ -48,8 +50,10 @@ contract("PercentageFeeHandler - [collectFee]", async (accounts) => { ).then((instance) => (ERC20MintableInstance = instance))), ]); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address diff --git a/test/handlers/fee/percentage/distributeFee.js b/test/handlers/fee/percentage/distributeFee.js index 74b528ca..264d31c5 100644 --- a/test/handlers/fee/percentage/distributeFee.js +++ b/test/handlers/fee/percentage/distributeFee.js @@ -6,6 +6,7 @@ const Helpers = require("../../../helpers"); const Ethers = require("ethers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const PercentageFeeHandlerContract = artifacts.require("PercentageERC20FeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); @@ -25,6 +26,7 @@ contract("PercentageFeeHandler - [distributeFee]", async (accounts) => { let BridgeInstance; let ERC20MintableInstance; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let PercentageFeeHandlerInstance; let FeeHandlerRouterInstance; @@ -62,8 +64,10 @@ contract("PercentageFeeHandler - [distributeFee]", async (accounts) => { originDomainID ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); await Promise.all([ diff --git a/test/handlers/xc20/deposit.js b/test/handlers/xc20/deposit.js index c7084ec2..cac61c88 100644 --- a/test/handlers/xc20/deposit.js +++ b/test/handlers/xc20/deposit.js @@ -149,10 +149,10 @@ contract("XC20Handler - [Deposit ERC20]", async (accounts) => { it(`When non-contract addresses are whitelisted in the handler, deposits which the addresses are set as a token address will be failed`, async () => { - const ZERO_Address = "0x0000000000000000000000000000000000000000"; + const NonContract_Address = "0x0000000000000000000000000000000000001111"; const EOA_Address = accounts[1]; - const resourceID_ZERO_Address = Helpers.createResourceID( - ZERO_Address, + const resourceID_NonContract_Address = Helpers.createResourceID( + NonContract_Address, originDomainID ); const resourceID_EOA_Address = Helpers.createResourceID( @@ -161,8 +161,8 @@ contract("XC20Handler - [Deposit ERC20]", async (accounts) => { ); await BridgeInstance.adminSetResource( XC20HandlerInstance.address, - resourceID_ZERO_Address, - ZERO_Address, + resourceID_NonContract_Address, + NonContract_Address, emptySetResourceData ); await BridgeInstance.adminSetResource( @@ -178,7 +178,7 @@ contract("XC20Handler - [Deposit ERC20]", async (accounts) => { await Helpers.reverts( BridgeInstance.deposit( destinationDomainID, - resourceID_ZERO_Address, + resourceID_NonContract_Address, Helpers.createERCDepositData( tokenAmount, lenRecipientAddress, diff --git a/test/helpers.js b/test/helpers.js index 9d4a373d..8857923b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -367,6 +367,9 @@ const expectToRevertWithCustomError = async function(promise, expectedErrorSigna try { await promise; } catch (error) { + if (!error.data) { + throw error; + } const encoded = web3.eth.abi.encodeFunctionSignature(expectedErrorSignature); const returnValue = error.data.result || error.data; // expect event error and provided error signatures to match @@ -408,6 +411,47 @@ const passes = async function(promise) { } } +const ACTIONS_ARRAY_ABI = +"tuple(uint256 nativeValue, address callTo, address approveTo, address tokenSend, address tokenReceive, bytes data)[]"; + +const createMessageCallData = function(transactionId, actions, receiver) { + return abiEncode( + ["bytes32", ACTIONS_ARRAY_ABI, "address"], + [ + transactionId, + actions.map(action => [ + action.nativeValue, + action.callTo, + action.approveTo, + action.tokenSend, + action.tokenReceive, + action.data, + ]), + receiver + ] + ) +} + +const createOptionalContractCallDepositData = function(amount, recipient, executionGasAmount, message) { + return ( + "0x" + + toHex(amount, 32).substr(2) + // uint256 + toHex(recipient.substr(2).length / 2, 32).substr(2) + // uint256 + recipient.substr(2) + // bytes + toHex(executionGasAmount, 32).substr(2) + // uint256 + toHex(message.substr(2).length / 2, 32).substr(2) + // uint256 + message.substr(2) // bytes + ) +} + +const getBalance = async (address) => { + return BigInt(await web3.eth.getBalance(address)); +}; + +const getTokenBalance = async (token, address) => { + return BigInt(await token.balanceOf(address)); +}; + module.exports = { advanceBlock, advanceTime, @@ -442,4 +486,8 @@ module.exports = { expectToRevertWithCustomError, reverts, passes, + createMessageCallData, + createOptionalContractCallDepositData, + getBalance, + getTokenBalance, }; diff --git a/testUnderForked/admin.js b/testUnderForked/admin.js index 0baf3de9..a3e05149 100644 --- a/testUnderForked/admin.js +++ b/testUnderForked/admin.js @@ -86,7 +86,8 @@ contract("TwapFeeHandler - [admin]", async (accounts) => { ); DynamicFeeHandlerInstance = await DynamicFeeHandlerContract.new( BridgeInstance.address, - FeeHandlerRouterInstance.address + FeeHandlerRouterInstance.address, + 0 ); ADMIN_ROLE = await DynamicFeeHandlerInstance.DEFAULT_ADMIN_ROLE(); }); diff --git a/testUnderForked/calculateFeeERC20EVM.js b/testUnderForked/calculateFeeERC20EVM.js index 4e2c865c..42628b69 100644 --- a/testUnderForked/calculateFeeERC20EVM.js +++ b/testUnderForked/calculateFeeERC20EVM.js @@ -54,6 +54,7 @@ contract("TwapFeeHandler - [calculateFee]", async (accounts) => { let QuoterInstance; let DynamicFeeHandlerInstance; let resourceID; + let depositData; beforeEach(async () => { await Promise.all([ @@ -100,7 +101,8 @@ contract("TwapFeeHandler - [calculateFee]", async (accounts) => { ); DynamicFeeHandlerInstance = await DynamicFeeHandlerContract.new( BridgeInstance.address, - FeeHandlerRouterInstance.address + FeeHandlerRouterInstance.address, + 0 ); FeeHandlerRouterInstance.adminSetResourceHandler( destinationDomainID, @@ -116,6 +118,12 @@ contract("TwapFeeHandler - [calculateFee]", async (accounts) => { ); await DynamicFeeHandlerInstance.setWrapTokenAddress(destinationDomainID, MATIC_ADDRESS); await DynamicFeeHandlerInstance.setFeeProperties(gasUsed); + + depositData = Helpers.createERCDepositData( + 100, + 20, + sender, + ); }); it("[fixed protocol fee] should get the correct values", async () => { @@ -125,7 +133,7 @@ contract("TwapFeeHandler - [calculateFee]", async (accounts) => { originDomainID, destinationDomainID, resourceID, - "0x00", + depositData, "0x00" ); @@ -161,7 +169,7 @@ contract("TwapFeeHandler - [calculateFee]", async (accounts) => { originDomainID, destinationDomainID, resourceID, - "0x00", + depositData, "0x00" ); diff --git a/testUnderForked/collectFeeERC20EVM.js b/testUnderForked/collectFeeERC20EVM.js index 5e8f8e55..35e971b5 100644 --- a/testUnderForked/collectFeeERC20EVM.js +++ b/testUnderForked/collectFeeERC20EVM.js @@ -7,6 +7,7 @@ const Ethers = require("ethers"); const Helpers = require("../test/helpers"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const DynamicFeeHandlerContract = artifacts.require("TwapNativeTokenFeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); @@ -52,6 +53,7 @@ contract("TwapNativeTokenFeeHandler - [collectFee]", async (accounts) => { let QuoterInstance; let DynamicFeeHandlerInstance; let resourceID; + let DefaultMessageReceiverInstance; let ERC20HandlerInstance; let ERC20MintableInstance; let depositData; @@ -68,15 +70,18 @@ contract("TwapNativeTokenFeeHandler - [collectFee]", async (accounts) => { ).then((instance) => (ERC20MintableInstance = instance))), ]); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); ERC20HandlerInstance = await ERC20HandlerContract.new( - BridgeInstance.address + BridgeInstance.address, + DefaultMessageReceiverInstance.address ); FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address ); DynamicFeeHandlerInstance = await DynamicFeeHandlerContract.new( BridgeInstance.address, - FeeHandlerRouterInstance.address + FeeHandlerRouterInstance.address, + 0 ); resourceID = Helpers.createResourceID( @@ -124,24 +129,22 @@ contract("TwapNativeTokenFeeHandler - [collectFee]", async (accounts) => { await DynamicFeeHandlerInstance.setWrapTokenAddress(destinationDomainID, MATIC_ADDRESS); await DynamicFeeHandlerInstance.setFeeProperties(gasUsed); - await Promise.all([ - BridgeInstance.adminSetResource( - ERC20HandlerInstance.address, - resourceID, - ERC20MintableInstance.address, - emptySetResourceData - ), - ERC20MintableInstance.mint(depositorAddress, tokenAmount), - ERC20MintableInstance.approve(ERC20HandlerInstance.address, tokenAmount, { - from: depositorAddress, - }), - BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), - FeeHandlerRouterInstance.adminSetResourceHandler( - destinationDomainID, - resourceID, - DynamicFeeHandlerInstance.address - ), - ]); + await BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID, + ERC20MintableInstance.address, + emptySetResourceData + ); + await ERC20MintableInstance.mint(depositorAddress, tokenAmount); + await ERC20MintableInstance.approve(ERC20HandlerInstance.address, tokenAmount, { + from: depositorAddress, + }); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + DynamicFeeHandlerInstance.address + ); depositData = Helpers.createERCDepositData( tokenAmount, diff --git a/testUnderForked/collectFeeGenericEVM.js b/testUnderForked/collectFeeGenericEVM.js index f3b6f02b..65046eb7 100644 --- a/testUnderForked/collectFeeGenericEVM.js +++ b/testUnderForked/collectFeeGenericEVM.js @@ -127,20 +127,18 @@ contract("TwapGenericFeeHandler - [collectFee]", async (accounts) => { ); await DynamicFeeHandlerInstance.setWrapTokenAddress(destinationDomainID, MATIC_ADDRESS); - await Promise.all([ - BridgeInstance.adminSetResource( - GmpHandlerInstance.address, - resourceID, - TestStoreInstance.address, - emptySetResourceData - ), - BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), - FeeHandlerRouterInstance.adminSetResourceHandler( - destinationDomainID, - resourceID, - DynamicFeeHandlerInstance.address - ), - ]); + await BridgeInstance.adminSetResource( + GmpHandlerInstance.address, + resourceID, + TestStoreInstance.address, + emptySetResourceData + ); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + DynamicFeeHandlerInstance.address + ); depositData = Helpers.createGmpDepositData( depositFunctionSignature, diff --git a/testUnderForked/optionalContractCall/calculateFeeERC20EVM.js b/testUnderForked/optionalContractCall/calculateFeeERC20EVM.js new file mode 100644 index 00000000..55e1648f --- /dev/null +++ b/testUnderForked/optionalContractCall/calculateFeeERC20EVM.js @@ -0,0 +1,178 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +const Ethers = require("ethers"); +const Helpers = require("../../test/helpers"); +const DynamicFeeHandlerContract = artifacts.require("TwapNativeTokenFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const TwapOracleContract = artifacts.require("TwapOracle"); + +const FACTORY_ABI = require( + "@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json" +).abi; +const FACTORY_BYTECODE = require( + "@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json" +).bytecode; +const POOL_ABI = require( + "@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json" +).abi; +const POOL_BYTECODE = require( + "@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json" +).bytecode; +const QUOTER_ABI = require( + "@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json" +).abi; +const QUOTER_BYTECODE = require( + "@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json" +).bytecode; + +contract("TwapFeeHandler - [calculateFee]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 3; + const gasUsed = 100000; + const gasPrice = 200000000000; + const ProtocolFeeType = { + None: "0", + Fixed: "1", + Percentage: "2" + } + const recipientAddress = accounts[2]; + + const depositAmount = Ethers.utils.parseEther("1"); + const fixedProtocolFee = Ethers.utils.parseEther("0.001"); + const transferredAmount = depositAmount.sub(fixedProtocolFee); + const sender = accounts[0]; + const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; + const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; + const MATIC_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"; + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const higherExecutionGasAmount = 30000000; + const lowerExecutionGasAmount = 3000000; + const feeData = "0x"; + + let UniswapFactoryInstance; + let TwapOracleInstance; + let BridgeInstance; + let FeeHandlerRouterInstance; + let pool_500; + let pool_3000; + let pool_10000; + let QuoterInstance; + let DynamicFeeHandlerInstance; + let resourceID; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + accounts[0] + )), + (ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ).then((instance) => (ERC20MintableInstance = instance))), + ]); + resourceID = Helpers.createResourceID( + ERC20MintableInstance.address, + originDomainID + ); + const provider = new Ethers.providers.JsonRpcProvider(); + const signer = provider.getSigner(); + UniswapFactoryInstance = new Ethers.ethers.ContractFactory( + new Ethers.ethers.utils.Interface(FACTORY_ABI), FACTORY_BYTECODE, signer + ); + UniswapFactoryInstance = await UniswapFactoryInstance.attach(UNISWAP_V3_FACTORY_ADDRESS); + + QuoterInstance = new Ethers.ethers.ContractFactory( + new Ethers.ethers.utils.Interface(QUOTER_ABI), QUOTER_BYTECODE, signer + ); + QuoterInstance = await QuoterInstance.deploy(UniswapFactoryInstance.address, WETH_ADDRESS); + + const poolFactory = new Ethers.ethers.ContractFactory( + new Ethers.ethers.utils.Interface(POOL_ABI), POOL_BYTECODE, signer + ); + pool_500 = await UniswapFactoryInstance.getPool(WETH_ADDRESS, MATIC_ADDRESS, 500); + pool_500 = await poolFactory.attach(pool_500); + pool_3000 = await UniswapFactoryInstance.getPool(WETH_ADDRESS, MATIC_ADDRESS, 3000); + pool_3000 = await poolFactory.attach(pool_3000); + pool_10000 = await UniswapFactoryInstance.getPool(WETH_ADDRESS, MATIC_ADDRESS, 10000); + pool_10000 = await poolFactory.attach(pool_10000); + + TwapOracleInstance = await TwapOracleContract.new(UniswapFactoryInstance.address, WETH_ADDRESS); + await TwapOracleInstance.setPool(MATIC_ADDRESS, 500, 100); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + DynamicFeeHandlerInstance = await DynamicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address, + 0 + ); + FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + DynamicFeeHandlerInstance.address + ), + await DynamicFeeHandlerInstance.setFeeOracle(TwapOracleInstance.address); + await DynamicFeeHandlerInstance.setGasPrice( + destinationDomainID, + gasPrice, // Polygon gas price is 200 Gwei + ProtocolFeeType.Fixed, + fixedProtocolFee + ); + await DynamicFeeHandlerInstance.setWrapTokenAddress(destinationDomainID, MATIC_ADDRESS); + await DynamicFeeHandlerInstance.setFeeProperties(gasUsed); + }); + + it("should return higher fee for higher execution amount", async () => { + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [recipientAddress, "20"]), + }] + const message = Helpers.createMessageCallData( + transactionId, + actions, + recipientAddress + ); + + const higherGasDepositData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + higherExecutionGasAmount, + message + ); + + const higherExecutionGasAmountRes = await FeeHandlerRouterInstance.calculateFee.call( + sender, + originDomainID, + destinationDomainID, + resourceID, + higherGasDepositData, + feeData + ); + + const lowerGasDepositData = Helpers.createOptionalContractCallDepositData( + transferredAmount, + Ethers.constants.AddressZero, + lowerExecutionGasAmount, + message + ); + + const lowerExecutionGasAmountRes = await FeeHandlerRouterInstance.calculateFee.call( + sender, + originDomainID, + destinationDomainID, + resourceID, + lowerGasDepositData, + feeData + ); + + expect(higherExecutionGasAmountRes.fee.toNumber()).to.be.gt(lowerExecutionGasAmountRes.fee.toNumber()) + }); +});