From 18cb7728aaa8dc3d8e1e5e4917c10d70440d6b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Mlinari=C4=87?= <35880252+nmlinaric@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:29:56 +0200 Subject: [PATCH] chore: optional contract call and default message receiver tests (#274) Co-authored-by: Oleksii Matiiasevych --- .../native/optionalContractCall/collectFee.js | 162 ++++++ .../optionalContractCall/decimalConversion.js | 255 +++++++++ .../native/optionalContractCall/deposit.js | 272 +++++++++ .../optionalContractCall/distributeFee.js | 285 ++++++++++ .../optionalContractCall/executeProposal.js | 409 +++++++++++++ test/defaultMessageReceiver/direct.js | 537 ++++++++++++++++++ .../defaultMessageReceiver/executeProposal.js | 263 +++++++++ .../erc20/optionalContracCall/collectFee.js | 159 ++++++ .../optionalContracCall/decimalConversion.js | 277 +++++++++ .../erc20/optionalContracCall/deposit.js | 211 +++++++ .../optionalContracCall/distributeFee.js | 279 +++++++++ .../optionalContracCall/executeProposal.js | 346 +++++++++++ test/helpers.js | 48 ++ .../calculateFeeERC20EVM.js | 178 ++++++ 14 files changed, 3681 insertions(+) create mode 100644 test/adapters/native/optionalContractCall/collectFee.js create mode 100644 test/adapters/native/optionalContractCall/decimalConversion.js create mode 100644 test/adapters/native/optionalContractCall/deposit.js create mode 100644 test/adapters/native/optionalContractCall/distributeFee.js create mode 100644 test/adapters/native/optionalContractCall/executeProposal.js create mode 100644 test/defaultMessageReceiver/direct.js create mode 100644 test/defaultMessageReceiver/executeProposal.js create mode 100644 test/handlers/erc20/optionalContracCall/collectFee.js create mode 100644 test/handlers/erc20/optionalContracCall/decimalConversion.js create mode 100644 test/handlers/erc20/optionalContracCall/deposit.js create mode 100644 test/handlers/erc20/optionalContracCall/distributeFee.js create mode 100644 test/handlers/erc20/optionalContracCall/executeProposal.js create mode 100644 testUnderForked/optionalContractCall/calculateFeeERC20EVM.js 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/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/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/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/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()) + }); +});