diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 986b68dd..990ac0cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Forked Mainnet Tests run: | fuser -k 8545/tcp - make start-forkedMainnet + make start-forkedMainnet FORKED_TESTS_PROVIDER=${{ secrets.FORKED_TESTS_PROVIDER }} npx truffle test testUnderForked/* coverage: diff --git a/Makefile b/Makefile index a7ef7cd1..8b952083 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ start-ganache: start-forkedMainnet: @echo " > \033[32mStarting forked environment... \033[0m " - ganache-cli -f https://eth-mainnet.g.alchemy.com/v2/34NZ4AoqM8OSolHSol6jh5xZSPq1rcL- & sleep 3 + ganache-cli -f $(FORKED_TESTS_PROVIDER) & sleep 3 start-geth: @echo " > \033[32mStarting geth... \033[0m " diff --git a/contracts/handlers/FeeHandlerRouter.sol b/contracts/handlers/FeeHandlerRouter.sol index e87451c9..bc9b6d65 100644 --- a/contracts/handlers/FeeHandlerRouter.sol +++ b/contracts/handlers/FeeHandlerRouter.sol @@ -91,7 +91,7 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl { feeHandler.collectFee{value: msg.value}(sender, fromDomainID, destinationDomainID, resourceID, depositData, feeData); } - /** + /** @notice Initiates calculating fee with corresponding fee handler contract using IFeeHandler interface. @param sender Sender of the deposit. @param fromDomainID ID of the source chain. diff --git a/contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol b/contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol index 14eba030..7ef7a4c5 100644 --- a/contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol +++ b/contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol @@ -18,7 +18,7 @@ contract DynamicERC20FeeHandlerEVMV2 is DynamicFeeHandlerV2 { constructor(address bridgeAddress, address feeHandlerRouterAddress) DynamicFeeHandlerV2(bridgeAddress, feeHandlerRouterAddress) { } - /** + /** @notice Calculates fee for transaction cost. @param destinationDomainID ID of chain deposit will be bridged to. @return fee Returns the fee amount. @@ -27,7 +27,15 @@ contract DynamicERC20FeeHandlerEVMV2 is DynamicFeeHandlerV2 { function _calculateFee(address, uint8, uint8 destinationDomainID, bytes32, bytes calldata, bytes calldata) internal view override returns (uint256 fee, address tokenAddress) { uint256 desintationCoinPrice = twapOracle.getPrice(destinationNativeCoinWrap[destinationDomainID]); if (desintationCoinPrice == 0) revert IncorrectPrice(); - uint256 txCost = destinationGasPrice[destinationDomainID] * _gasUsed * desintationCoinPrice / 1e18; + Fee memory destFeeConfig = destinationFee[destinationDomainID]; + + uint256 txCost = destFeeConfig.gasPrice * _gasUsed * desintationCoinPrice / 1e18; + if(destFeeConfig.feeType == ProtocolFeeType.Fixed) { + txCost += destFeeConfig.amount; + } else if (destFeeConfig.feeType == ProtocolFeeType.Percentage) { + txCost += txCost * destFeeConfig.amount / 1e4; // 100 for percent and 100 to avoid precision loss; + } + return (txCost, address(0)); } } diff --git a/contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol b/contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol index 226c80d3..7b610131 100644 --- a/contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol +++ b/contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol @@ -20,11 +20,24 @@ abstract contract DynamicFeeHandlerV2 is IFeeHandler, AccessControl { address public immutable _feeHandlerRouterAddress; TwapOracle public twapOracle; + ProtocolFeeType public protocolFeeType; uint32 public _gasUsed; mapping(uint8 => address) public destinationNativeCoinWrap; - mapping(uint8 => uint256) public destinationGasPrice; + mapping(uint8 => Fee) public destinationFee; + + enum ProtocolFeeType { + None, + Fixed, + Percentage + } + + struct Fee { + uint256 gasPrice; + ProtocolFeeType feeType; + uint248 amount; + } event FeeOracleAddressSet(TwapOracle feeOracleAddress); event FeePropertySet(uint32 gasUsed); @@ -95,9 +108,17 @@ abstract contract DynamicFeeHandlerV2 is IFeeHandler, AccessControl { @notice Sets the gas price for destination chain. @param destinationDomainID ID of destination chain. @param gasPrice Gas price of destination chain. + @param feeType Type of fee that can be set (fixed/percentage). + "0" => execution cost + "1" => execution cost + protocol fee (fixed fee) + "2" => execution cost + protocol fee (percentage fee) + @param amount Fee amount that should be additional charged on top of + execution cost (fixed native token amount/percentage of execution cost). */ - function setGasPrice(uint8 destinationDomainID, uint256 gasPrice) external onlyAdmin { - destinationGasPrice[destinationDomainID] = gasPrice; + function setGasPrice(uint8 destinationDomainID, uint256 gasPrice, ProtocolFeeType feeType, uint248 amount) external onlyAdmin { + destinationFee[destinationDomainID].gasPrice = gasPrice; + destinationFee[destinationDomainID].feeType = feeType; + destinationFee[destinationDomainID].amount = amount; emit GasPriceSet(destinationDomainID, gasPrice); } @@ -140,7 +161,7 @@ abstract contract DynamicFeeHandlerV2 is IFeeHandler, AccessControl { emit FeeCollected(sender, fromDomainID, destinationDomainID, resourceID, fee, address(0)); } - /** + /** @notice Calculates fee for deposit. @param sender Sender of the deposit. @param fromDomainID ID of the source chain. diff --git a/contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol b/contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol index fc844179..92a6b71d 100644 --- a/contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol +++ b/contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol @@ -18,7 +18,7 @@ contract DynamicGenericFeeHandlerEVMV2 is DynamicFeeHandlerV2 { constructor(address bridgeAddress, address feeHandlerRouterAddress) DynamicFeeHandlerV2(bridgeAddress, feeHandlerRouterAddress) { } - /** + /** @notice Calculates fee for transaction cost. @param destinationDomainID ID of chain deposit will be bridged to. @param depositData Additional data to be passed to specified handler. @@ -29,7 +29,15 @@ contract DynamicGenericFeeHandlerEVMV2 is DynamicFeeHandlerV2 { uint256 maxFee = uint256(bytes32(depositData[:32])); uint256 desintationCoinPrice = twapOracle.getPrice(destinationNativeCoinWrap[destinationDomainID]); if (desintationCoinPrice == 0) revert IncorrectPrice(); - uint256 txCost = destinationGasPrice[destinationDomainID] * maxFee * desintationCoinPrice / 1e18; + Fee memory destFeeConfig = destinationFee[destinationDomainID]; + + uint256 txCost = destFeeConfig.gasPrice * maxFee * desintationCoinPrice / 1e18; + if(destFeeConfig.feeType == ProtocolFeeType.Fixed) { + txCost += destFeeConfig.amount; + } else if (destFeeConfig.feeType == ProtocolFeeType.Percentage) { + txCost += txCost * destFeeConfig.amount / 1e4; // 100 for percent and 100 to avoid precision loss; + } + return (txCost, address(0)); } } diff --git a/testUnderForked/calculateFeeERC20EVM.js b/testUnderForked/calculateFeeERC20EVM.js index 817b8915..e7cdc62f 100644 --- a/testUnderForked/calculateFeeERC20EVM.js +++ b/testUnderForked/calculateFeeERC20EVM.js @@ -31,6 +31,13 @@ contract("DynamicFeeHandlerV2 - [calculateFee]", async (accounts) => { const destinationDomainID = 3; const gasUsed = 100000; const gasPrice = 200000000000; + const ProtocolFeeType = { + None: "0", + Fixed: "1", + Percentage: "2" + } + const fixedProtocolFee = Ethers.utils.parseEther("0.001"); + const feePercentage = 1000; // 10% const sender = accounts[0]; const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; @@ -101,12 +108,17 @@ contract("DynamicFeeHandlerV2 - [calculateFee]", async (accounts) => { DynamicFeeHandlerInstance.address ), await DynamicFeeHandlerInstance.setFeeOracle(TwapOracleInstance.address); - await DynamicFeeHandlerInstance.setGasPrice(destinationDomainID, gasPrice); // Polygon gas price is 200 Gwei + 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 get the correct values", async () => { + it("[fixed protocol fee] should get the correct values", async () => { const feeInDestinationToken = gasPrice * gasUsed; const res = await FeeHandlerRouterInstance.calculateFee.call( sender, @@ -119,13 +131,48 @@ contract("DynamicFeeHandlerV2 - [calculateFee]", async (accounts) => { const input = new Ethers.ethers.BigNumber.from(feeInDestinationToken.toString()); const out = await QuoterInstance.callStatic.quoteExactInputSingle(MATIC_ADDRESS, WETH_ADDRESS, 500, input, 0); - expect(res.fee.toNumber()).to.be.within(out*0.99, out*1.01); + expect( + (await DynamicFeeHandlerInstance.destinationFee(destinationDomainID)).feeType.toString() + ).to.be.equal(ProtocolFeeType.Fixed); + expect(res.fee.toNumber()).to.be.within( + out*0.99 + Number(fixedProtocolFee), + out*1.01 + Number(fixedProtocolFee) + ); }); it("should get the correct price for the tokens with no available pool", async () => { - const bnb_price = Ethers.utils.parseEther("0.18"); - await TwapOracleInstance.setPrice(BNB_ADDRESS, bnb_price); - const priceOnOracle = await TwapOracleInstance.getPrice(BNB_ADDRESS); - assert.equal(priceOnOracle.toString(), bnb_price.toString()); + const bnb_price = Ethers.utils.parseEther("0.18"); + await TwapOracleInstance.setPrice(BNB_ADDRESS, bnb_price); + const priceOnOracle = await TwapOracleInstance.getPrice(BNB_ADDRESS); + assert.equal(priceOnOracle.toString(), bnb_price.toString()); + }); + + it("[percentage protocol fee] should get the correct values", async () => { + await DynamicFeeHandlerInstance.setGasPrice( + destinationDomainID, + gasPrice, // Polygon gas price is 200 Gwei + ProtocolFeeType.Percentage, + feePercentage + ); + + const feeInDestinationToken = gasPrice * gasUsed; + const res = await FeeHandlerRouterInstance.calculateFee.call( + sender, + originDomainID, + destinationDomainID, + resourceID, + "0x00", + "0x00" + ); + + const input = new Ethers.ethers.BigNumber.from(feeInDestinationToken.toString()); + const out = await QuoterInstance.callStatic.quoteExactInputSingle(MATIC_ADDRESS, WETH_ADDRESS, 500, input, 0); + expect( + (await DynamicFeeHandlerInstance.destinationFee(destinationDomainID)).feeType.toString() + ).to.be.equal(ProtocolFeeType.Percentage); + expect(res.fee.toNumber()).to.be.within( + out*1.09, + out*1.11 + ); }); }); diff --git a/testUnderForked/collectFeeERC20EVM.js b/testUnderForked/collectFeeERC20EVM.js index 61e4fc23..cbc1b3eb 100644 --- a/testUnderForked/collectFeeERC20EVM.js +++ b/testUnderForked/collectFeeERC20EVM.js @@ -31,6 +31,12 @@ contract("DynamicERC20FeeHandlerEVMV2 - [collectFee]", async (accounts) => { const destinationDomainID = 3; const gasUsed = 100000; const gasPrice = 200000000000; + const ProtocolFeeType = { + None: "0", + Fixed: "1", + Percentage: "2" + } + const fixedProtocolFee = Ethers.utils.parseEther("0.001"); const sender = accounts[0]; const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; @@ -109,7 +115,12 @@ contract("DynamicERC20FeeHandlerEVMV2 - [collectFee]", async (accounts) => { DynamicFeeHandlerInstance.address ); await DynamicFeeHandlerInstance.setFeeOracle(TwapOracleInstance.address); - await DynamicFeeHandlerInstance.setGasPrice(destinationDomainID, gasPrice); // Polygon gas price is 200 Gwei + await DynamicFeeHandlerInstance.setGasPrice( + destinationDomainID, + gasPrice, // Polygon gas price is 200 Gwei + ProtocolFeeType.Fixed, + fixedProtocolFee + ); await DynamicFeeHandlerInstance.setWrapTokenAddress(destinationDomainID, MATIC_ADDRESS); await DynamicFeeHandlerInstance.setFeeProperties(gasUsed); @@ -225,7 +236,7 @@ contract("DynamicERC20FeeHandlerEVMV2 - [collectFee]", async (accounts) => { it("deposit should revert if the destination coin's price is 0", async () => { const fee = Ethers.utils.parseEther("1.0"); - await TwapOracleInstance.setPrice(MATIC_ADDRESS, 0); + await TwapOracleInstance.setPrice(MATIC_ADDRESS, 0); await Helpers.expectToRevertWithCustomError( BridgeInstance.deposit( diff --git a/testUnderForked/collectFeeGenericEVM.js b/testUnderForked/collectFeeGenericEVM.js index cff0f397..269e093c 100644 --- a/testUnderForked/collectFeeGenericEVM.js +++ b/testUnderForked/collectFeeGenericEVM.js @@ -29,6 +29,12 @@ contract("DynamicGenericFeeHandlerEVMV2 - [collectFee]", async (accounts) => { const originDomainID = 1; const destinationDomainID = 3; const gasPrice = 200000000000; + const ProtocolFeeType = { + None: "0", + Fixed: "1", + Percentage: "2" + } + const fixedProtocolFee = Ethers.utils.parseEther("0.001"); const sender = accounts[0]; const UNISWAP_V3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; @@ -113,7 +119,12 @@ contract("DynamicGenericFeeHandlerEVMV2 - [collectFee]", async (accounts) => { DynamicFeeHandlerInstance.address ); await DynamicFeeHandlerInstance.setFeeOracle(TwapOracleInstance.address); - await DynamicFeeHandlerInstance.setGasPrice(destinationDomainID, gasPrice); // Polygon gas price is 200 Gwei + await DynamicFeeHandlerInstance.setGasPrice( + destinationDomainID, + gasPrice, // Polygon gas price is 200 Gwei + ProtocolFeeType.Fixed, + fixedProtocolFee + ); await DynamicFeeHandlerInstance.setWrapTokenAddress(destinationDomainID, MATIC_ADDRESS); await Promise.all([ @@ -226,7 +237,7 @@ contract("DynamicGenericFeeHandlerEVMV2 - [collectFee]", async (accounts) => { it("deposit should revert if the destination coin's price is 0", async () => { const fee = Ethers.utils.parseEther("1.0"); - await TwapOracleInstance.setPrice(MATIC_ADDRESS, 0); + await TwapOracleInstance.setPrice(MATIC_ADDRESS, 0); await Helpers.expectToRevertWithCustomError( BridgeInstance.deposit(