diff --git a/CHANGELOG.md b/CHANGELOG.md index b7486443..2c076489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [2.9.0](https://github.com/sygmaprotocol/sygma-solidity/compare/v2.8.0...v2.9.0) (2024-07-30) + + +### Features + +* add native token handler and adapter ([#254](https://github.com/sygmaprotocol/sygma-solidity/issues/254)) ([d5cea35](https://github.com/sygmaprotocol/sygma-solidity/commit/d5cea359a5304113e10283bd5ea4e4070284b6d9)) + + +### Bug Fixes + +* make frost keygen retryable ([#251](https://github.com/sygmaprotocol/sygma-solidity/issues/251)) ([88fd294](https://github.com/sygmaprotocol/sygma-solidity/commit/88fd29458afe10ae2ee8994e992570b7d668c946)) + + +### Miscellaneous + +* update what gets pushed to npm package ([#258](https://github.com/sygmaprotocol/sygma-solidity/issues/258)) ([faa73ae](https://github.com/sygmaprotocol/sygma-solidity/commit/faa73aea87a1b3bd228f652481733d1592fc49f6)) + ## [2.8.0](https://github.com/sygmaprotocol/sygma-solidity/compare/v2.7.0...v2.8.0) (2024-06-07) diff --git a/contracts/FROSTKeygen.sol b/contracts/FROSTKeygen.sol index 2776723b..8243b95c 100644 --- a/contracts/FROSTKeygen.sol +++ b/contracts/FROSTKeygen.sol @@ -7,20 +7,27 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract FROSTKeygen is Ownable { - bool private keygenStarted; - event StartedFROSTKeygen(); + bool private keygenEnded = false; + + event StartedFROSTKeygen(); + event EndedFROSTKeygen(); - modifier onlyOnce(){ - require (!keygenStarted, "FROST keygen can be called only once"); - _; - keygenStarted = true; - } - /** @notice Emits {StartedFROSTKeygen} event */ - function startFROSTKeygen() public onlyOwner onlyOnce { + function startFROSTKeygen() public onlyOwner { + require (!keygenEnded, "FROST keygen ended"); + emit StartedFROSTKeygen(); } + /** + @notice Blocks further calls for starting keygen. + */ + function endFROSTKeygen() public onlyOwner { + keygenEnded = true; + + emit EndedFROSTKeygen(); + } + } \ No newline at end of file diff --git a/contracts/Retry.sol b/contracts/Retry.sol new file mode 100644 index 00000000..b37293bd --- /dev/null +++ b/contracts/Retry.sol @@ -0,0 +1,24 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity 0.8.11; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Retry is Ownable { + + event Retry(uint8 sourceDomainID, uint8 destinationDomainID, uint256 blockHeight, bytes32 resourceID); + + /** + @notice This method is used to trigger the process for retrying failed deposits on the MPC side. + @notice Only callable by admin. + @param sourceDomainID ID of the retry source. + @param destinationDomainID ID of the transfer destination. + @param blockHeight Block height on origin chain which contains failed deposits. + @param resourceID Resource ID of transfers that are to be retried. + */ + function retry(uint8 sourceDomainID, uint8 destinationDomainID, uint256 blockHeight, bytes32 resourceID) external onlyOwner { + emit Retry(sourceDomainID, destinationDomainID, blockHeight, resourceID); + } + +} \ No newline at end of file diff --git a/contracts/adapters/nativeTokens/NativeTokenAdapter.sol b/contracts/adapters/nativeTokens/NativeTokenAdapter.sol new file mode 100644 index 00000000..22a0b59f --- /dev/null +++ b/contracts/adapters/nativeTokens/NativeTokenAdapter.sol @@ -0,0 +1,62 @@ +// 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 { + 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(); + _; + } + + 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); + address feeHandlerRouter = _bridge._feeHandler(); + (uint256 fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + _resourceID, + "", // depositData - not parsed + "" // feeData - not parsed + ); + + if (msg.value < fee) revert MsgValueLowerThanFee(msg.value); + uint256 transferAmount = msg.value - fee; + + bytes memory depositData = abi.encodePacked( + transferAmount, + bytes(recipientAddress).length, + recipientAddress + ); + + _bridge.deposit{value: fee}(destinationDomainID, _resourceID, depositData, ""); + + address nativeHandlerAddress = _bridge._resourceIDToHandlerAddress(_resourceID); + (bool success, ) = nativeHandlerAddress.call{value: transferAmount}(""); + if(!success) revert FailedFundsTransfer(); + } + + receive() external payable {} +} diff --git a/contracts/handlers/FeeHandlerRouter.sol b/contracts/handlers/FeeHandlerRouter.sol index 580dc88d..7617fe8e 100644 --- a/contracts/handlers/FeeHandlerRouter.sol +++ b/contracts/handlers/FeeHandlerRouter.sol @@ -58,7 +58,7 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl { _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } /** - @notice Maps the {handlerAddress} to {resourceID} to {destinantionDomainID} in {_domainResourceIDToFeeHandlerAddress}. + @notice Maps the {handlerAddress} to {resourceID} to {destinationDomainID} in {_domainResourceIDToFeeHandlerAddress}. @param destinationDomainID ID of chain FeeHandler contracts will be called. @param resourceID ResourceID for which the corresponding FeeHandler will collect/calcualte fee. @param handlerAddress Address of FeeHandler which will be called for specified resourceID. diff --git a/contracts/handlers/NativeTokenHandler.sol b/contracts/handlers/NativeTokenHandler.sol new file mode 100644 index 00000000..0b8c8815 --- /dev/null +++ b/contracts/handlers/NativeTokenHandler.sol @@ -0,0 +1,124 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +import "../interfaces/IHandler.sol"; +import "./ERCHandlerHelpers.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 { + + address public immutable _nativeTokenAdapterAddress; + + /** + @param bridgeAddress Contract address of previously deployed Bridge. + */ + constructor( + address bridgeAddress, + address nativeTokenAdapterAddress + ) ERCHandlerHelpers(bridgeAddress) { + _nativeTokenAdapterAddress = nativeTokenAdapterAddress; + } + + event Withdrawal(address recipient, uint256 amount); + event FundsTransferred(address recipient, uint256 amount); + + error FailedFundsTransfer(); + error InsufficientBalance(); + error InvalidSender(address sender); + + /** + @notice A deposit is initiated by making a deposit to the NativeTokenAdapter which constructs the required + deposit data and propagates it to 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. + @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. + */ + function deposit( + bytes32 resourceID, + address depositor, + bytes calldata data + ) external 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)); + } + + /** + @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 {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 + */ + 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); + + (bool success, ) = address(recipientAddress).call{value: convertedAmount}(""); + if(!success) revert FailedFundsTransfer(); + emit FundsTransferred(recipientAddress, amount); + + return abi.encode(tokenAddress, address(recipientAddress), convertedAmount); + } + + /** + @notice Used to manually release ERC20 tokens from ERC20Safe. + @param data Consists of {tokenAddress}, {recipient}, and {amount} all padded to 32 bytes. + @notice Data passed into the function should be constructed as follows: + tokenAddress address bytes 0 - 32 + recipient address bytes 32 - 64 + amount uint bytes 64 - 96 + */ + function withdraw(bytes memory data) external override onlyBridge { + address recipient; + uint amount; + + if (address(this).balance <= amount) revert InsufficientBalance(); + (, recipient, amount) = abi.decode(data, (address, address, uint)); + + (bool success, ) = address(recipient).call{value: amount}(""); + if(!success) revert FailedFundsTransfer(); + emit Withdrawal(recipient, amount); + } + + /** + @notice Sets {_resourceIDToContractAddress} with {contractAddress}, + {_tokenContractAddressToTokenProperties[tokenAddress].resourceID} with {resourceID} and + {_tokenContractAddressToTokenProperties[tokenAddress].isWhitelisted} to true for {contractAddress} in ERCHandlerHelpers contract. + 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 passed to the handler - this should be 1 byte containing number of decimals places. + */ + function setResource(bytes32 resourceID, address contractAddress, bytes calldata args) external onlyBridge { + _setResource(resourceID, contractAddress); + + if (args.length > 0) { + uint8 externalTokenDecimals = uint8(bytes1(args)); + _setDecimals(contractAddress, externalTokenDecimals); + } + } + + receive() external payable {} +} diff --git a/contracts/handlers/fee/BasicFeeHandler.sol b/contracts/handlers/fee/BasicFeeHandler.sol index 9b839b53..c326bfc6 100644 --- a/contracts/handlers/fee/BasicFeeHandler.sol +++ b/contracts/handlers/fee/BasicFeeHandler.sol @@ -99,11 +99,11 @@ contract BasicFeeHandler is IFeeHandler, AccessControl { } /** - @notice Maps the {newFee} to {destinantionDomainID} to {resourceID} in {_domainResourceIDToFee}. + @notice Maps the {newFee} to {destinationDomainID} to {resourceID} in {_domainResourceIDToFee}. @notice Only callable by admin. @param destinationDomainID ID of chain fee will be set. @param resourceID ResourceID for which fee will be set. - @param newFee Value to which fee will be updated to for the provided {destinantionDomainID} and {resourceID}. + @param newFee Value to which fee will be updated to for the provided {destinationDomainID} and {resourceID}. */ function changeFee(uint8 destinationDomainID, bytes32 resourceID, uint256 newFee) external onlyAdmin { uint256 currentFee = _domainResourceIDToFee[destinationDomainID][resourceID]; diff --git a/contracts/interfaces/IBasicFeeHandler.sol b/contracts/interfaces/IBasicFeeHandler.sol new file mode 100644 index 00000000..b0c05568 --- /dev/null +++ b/contracts/interfaces/IBasicFeeHandler.sol @@ -0,0 +1,11 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +interface IBasicFeeHandler { + + /** + @notice Exposes getter function for _domainResourceIDToFee + */ + function _domainResourceIDToFee(uint8 destinationDomainID, bytes32 resourceID) pure external returns (uint256); +} diff --git a/migrations/10_retry.js b/migrations/10_retry.js new file mode 100644 index 00000000..3358a4c1 --- /dev/null +++ b/migrations/10_retry.js @@ -0,0 +1,10 @@ +const RetryContract = artifacts.require("Retry"); + +module.exports = async function (deployer) { + await deployer.deploy(RetryContract); + const RetryInstance = await RetryContract.deployed(); + + console.table({ + "Retry Address": RetryInstance.address, + }); +} diff --git a/package.json b/package.json index ed8c3b5b..dc651034 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@buildwithsygma/sygma-contracts", - "version": "2.8.0", + "version": "2.9.0", "description": "", "main": "dist/index.js", "repository": "https://github.com/sygmaprotocol/sygma-solidity.git", @@ -11,11 +11,14 @@ "build/contracts/ERC721Handler.json", "build/contracts/ERC1155Handler.json", "build/contracts/GmpHandler.json", - "build/contract/FeeHandlerRouter.json", "build/contracts/BasicFeeHandler.json", "build/contracts/PercentageERC20FeeHandler.json", - "build/contracts/DynamicGenericFeeHandler.json", - "build/contracts/DynamicERC20FeeHandler.json", + "build/contracts/PercentageERC20FeeHandler.json", + "build/contracts/TwapOracle.json", + "build/contracts/TwapFeeHandler.json", + "build/contracts/FeeHandlerRouter.json", + "build/contracts/NativeTokenAdapter.json", + "build/contracts/NativeTokenHandler.json", "contracts/interfaces" ], "directories": { diff --git a/test/adapters/native/collectFee.js b/test/adapters/native/collectFee.js new file mode 100644 index 00000000..666e14f8 --- /dev/null +++ b/test/adapters/native/collectFee.js @@ -0,0 +1,113 @@ +// 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 NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); + +contract("Bridge - [collect fee - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[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); + + let BridgeInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + + 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 + ); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + ); + + 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 + ), + + // 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.deposit( + destinationDomainID, + btcRecipientAddress, + { + 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/decimalConversion.js b/test/adapters/native/decimalConversion.js new file mode 100644 index 00000000..b325d3e4 --- /dev/null +++ b/test/adapters/native/decimalConversion.js @@ -0,0 +1,199 @@ +// 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 NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); + +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 btcRecipientAddress = "bc1qs0fcdq73vgurej48yhtupzcv83un2p5qhsje7n"; + 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 AbiCoder = new Ethers.utils.AbiCoder(); + const expectedDepositData = Helpers.createBtcDepositData(transferredAmount, btcRecipientAddress); + const expectedHandlerResponse = AbiCoder.encode( + ["uint256"], + [convertedTransferAmount] + ); + + let BridgeInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let depositProposalData; + + 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 + ); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + originDecimalPlaces + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + + depositProposalData = Helpers.createERCDepositData( + transferredAmount, + 20, + btcRecipientAddress + ); + + proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + resourceID: resourceID, + data: depositProposalData, + }; + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: NativeTokenHandlerInstance.address, + value: "1000000000000000000" + }) + }); + + + it("[sanity] decimals value is set if args are provided to 'adminSetResource'", async () => { + const NativeTokenDecimals = (await NativeTokenHandlerInstance._tokenContractAddressToTokenProperties.call( + NativeTokenHandlerInstance.address + )).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.deposit(destinationDomainID, btcRecipientAddress, { + 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 === expectedDepositData && + 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.createERCDepositData( + convertedTransferAmount, // 18 decimals + 20, + evmRecipientAddress + ); + + 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} + ); + + 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"], + [NativeTokenHandlerInstance.address, evmRecipientAddress, expectedRecipientTransferAmount] + ) + ); + }); + + // 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/deposit.js b/test/adapters/native/deposit.js new file mode 100644 index 00000000..76bc8f32 --- /dev/null +++ b/test/adapters/native/deposit.js @@ -0,0 +1,165 @@ +// 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 NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); + +contract("Bridge - [deposit - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + + 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); + + let BridgeInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + + 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 + ); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + ); + + 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 + ), + + // 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("_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 TruffleAssert.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, + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + + await TruffleAssert.reverts( + NativeTokenAdapterInstance.deposit(destinationDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + }); +}); diff --git a/test/adapters/native/distributeFee.js b/test/adapters/native/distributeFee.js new file mode 100644 index 00000000..5d9fd288 --- /dev/null +++ b/test/adapters/native/distributeFee.js @@ -0,0 +1,241 @@ +// 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 NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); + +contract("Native token adapter - [distributeFee]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + + const depositorAddress = accounts[1]; + const recipientAddress = accounts[2]; + + 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 assertOnlyAdmin = (method, ...params) => { + return TruffleAssert.fails( + method(...params, {from: accounts[1]}), + "sender doesn't have admin role" + ); + }; + + let BridgeInstance; + let NativeTokenHandlerInstance; + let NativeTokenAdapterInstance; + let FeeHandlerRouterInstance; + let BasicFeeHandlerInstance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + originDomainID, + accounts[0] + )) + ]); + + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID + ); + + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + + depositData = Helpers.createERCDepositData( + depositAmount, + 20, + recipientAddress + ); + + // 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.deposit( + destinationDomainID, + btcRecipientAddress, + { + 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.deposit( + destinationDomainID, + btcRecipientAddress, + { + 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.deposit( + destinationDomainID, + btcRecipientAddress, + { + 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/executeProposal.js b/test/adapters/native/executeProposal.js new file mode 100644 index 00000000..651c45a6 --- /dev/null +++ b/test/adapters/native/executeProposal.js @@ -0,0 +1,258 @@ +// 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 NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); + +contract("Bridge - [execute proposal - native token]", async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const adminAddress = accounts[0]; + const depositorAddress = accounts[1]; + const recipientAddress = accounts[2]; + const relayer1Address = accounts[3]; + + const expectedDepositNonce = 1; + const 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); + + let BridgeInstance; + let NativeTokenHandlerInstance; + let BasicFeeHandlerInstance; + let FeeHandlerRouterInstance; + let NativeTokenAdapterInstance; + let proposal; + let depositProposalData; + let dataHash; + + 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 + ); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapterInstance.address, + ); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + originDomainID, + resourceID, + BasicFeeHandlerInstance.address + ), + + depositProposalData = Helpers.createERCDepositData( + transferredAmount, + 20, + recipientAddress + ); + + 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); + + // send ETH to destination adapter for transfers + await web3.eth.sendTransaction({ + from: depositorAddress, + to: NativeTokenHandlerInstance.address, + value: "1000000000000000000" + }) + }); + + it("isProposalExecuted returns false if depositNonce is not used", async () => { + const destinationDomainID = await BridgeInstance._domainID(); + + assert.isFalse( + await BridgeInstance.isProposalExecuted( + destinationDomainID, + expectedDepositNonce + ) + ); + }); + + it("should create and execute executeProposal successfully", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.deposit(originDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + + const recipientBalanceBefore = await web3.eth.getBalance(recipientAddress); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }) + ); + + // 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(recipientAddress); + assert.strictEqual(transferredAmount.add(recipientBalanceBefore).toString(), recipientBalanceAfter); + }); + + 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.deposit(originDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + + await TruffleAssert.passes( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }) + ); + + const skipExecuteTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + // check that no ProposalExecution events are emitted + assert.equal(skipExecuteTx.logs.length, 0); + }); + + it("executeProposal event should be emitted with expected values", async () => { + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // depositorAddress makes initial deposit of depositAmount + assert.isFalse(await BridgeInstance.paused()); + await TruffleAssert.passes( + NativeTokenAdapterInstance.deposit(originDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + + const recipientBalanceBefore = await web3.eth.getBalance(recipientAddress); + + const proposalTx = await BridgeInstance.executeProposal( + proposal, + proposalSignedData, + {from: relayer1Address} + ); + + TruffleAssert.eventEmitted(proposalTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce && + event.dataHash === dataHash && + event.handlerResponse === Ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256"], + [NativeTokenHandlerInstance.address, recipientAddress, 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(recipientAddress); + 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.deposit(originDomainID, btcRecipientAddress, { + from: depositorAddress, + value: depositAmount + }) + ); + + await Helpers.expectToRevertWithCustomError( + BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }), + "InvalidProposalSigner()" + ); + }); +}); diff --git a/test/frostKeygen/frostKeygen.js b/test/frostKeygen/frostKeygen.js index 6fb2d235..7c7fe599 100644 --- a/test/frostKeygen/frostKeygen.js +++ b/test/frostKeygen/frostKeygen.js @@ -13,7 +13,6 @@ contract("FROSTKeygen", (accounts) => { }); it("should emit StartedFROSTKeygen event when startFROSTKeygen is called by the owner", async () => { - const tx = await FROSTKeygenInstance.startFROSTKeygen({from: accounts[0]}) TruffleAssert.eventEmitted(tx, "StartedFROSTKeygen"); @@ -21,20 +20,23 @@ contract("FROSTKeygen", (accounts) => { }); it("should revert when startFROSTKeygen is not called by the owner", async () => { - await Helpers.reverts( FROSTKeygenInstance.startFROSTKeygen({from: accounts[1]}), ) }); - it("should revert when startFROSTKeygen is called more than once", async() => { + it("should revert when keygen ended", async() => { + const tx = await FROSTKeygenInstance.endFROSTKeygen({from: accounts[0]}) + TruffleAssert.eventEmitted(tx, "EndedFROSTKeygen"); - const tx = await FROSTKeygenInstance.startFROSTKeygen({from: accounts[0]}) - - TruffleAssert.eventEmitted(tx, "StartedFROSTKeygen"); + await TruffleAssert.reverts( + FROSTKeygenInstance.startFROSTKeygen({from: accounts[1]}), + ) + }); + it("should revert when end keygen not called by owner", async() => { await Helpers.reverts( FROSTKeygenInstance.startFROSTKeygen({from: accounts[0]})) - }) + }); }) diff --git a/test/helpers.js b/test/helpers.js index 112a2f69..7a67e24e 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -108,6 +108,16 @@ const createERC721DepositProposalData = ( ); // metaData (?? bytes) }; +const createBtcDepositData = ( + transferAmount, + btcRecipientAddress +) => { + return Ethers.utils.solidityPack( + ["uint256", "uint256", "string"], + [transferAmount, btcRecipientAddress.length, btcRecipientAddress] + ) +} + const advanceBlock = () => { const provider = new Ethers.providers.JsonRpcProvider(); const time = Math.floor(Date.now() / 1000); @@ -416,6 +426,7 @@ module.exports = { createERC1155DepositProposalData, createERC1155WithdrawData, createGmpDepositData, + createBtcDepositData, constructGenericHandlerSetResourceData, createERC721DepositProposalData, createResourceID, diff --git a/test/retry/retry.js b/test/retry/retry.js new file mode 100644 index 00000000..2ffdaf6b --- /dev/null +++ b/test/retry/retry.js @@ -0,0 +1,43 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Retry = artifacts.require("Retry") + +contract("Retry", (accounts) => { + let RetryInstance; + + const sourceDomainID = 1; + const destinationDomainID = 2; + const blockHeight = 15; + const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000300"; + + beforeEach(async () => { + RetryInstance = await Retry.new(accounts[0]); + }); + + it("should emit Retry event when retry is called by the owner", async () => { + const tx = await RetryInstance.retry( + sourceDomainID, + destinationDomainID, + blockHeight, + resourceID, + {from: accounts[0]}) + + TruffleAssert.eventEmitted(tx, "Retry", (event) => { + return ( + event.sourceDomainID.toNumber() === sourceDomainID && + event.destinationDomainID.toNumber() === destinationDomainID && + event.blockHeight.toNumber() === blockHeight && + event.resourceID === resourceID + ); + }); + }); + + it("should revert when retry is not called by the owner", async () => { + await TruffleAssert.reverts( + RetryInstance.retry(sourceDomainID, destinationDomainID, blockHeight, resourceID, {from: accounts[1]}), + "Ownable: caller is not the owner." + ) + }); +})