diff --git a/contracts/handlers/FeeHandlerRouter.sol b/contracts/handlers/FeeHandlerRouter.sol index e0e41eef..e87451c9 100644 --- a/contracts/handlers/FeeHandlerRouter.sol +++ b/contracts/handlers/FeeHandlerRouter.sol @@ -15,11 +15,16 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl { // destination domainID => resourceID => feeHandlerAddress mapping (uint8 => mapping(bytes32 => IFeeHandler)) public _domainResourceIDToFeeHandlerAddress; + // whitelisted address => is whitelisted + mapping(address => bool) public _whitelist; - event FeeChanged( - uint256 newFee + event WhitelistChanged( + address whitelistAddress, + bool isWhitelisted ); + error IncorrectFeeSupplied(uint256); + modifier onlyBridge() { _onlyBridge(); _; @@ -55,6 +60,17 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl { _domainResourceIDToFeeHandlerAddress[destinationDomainID][resourceID] = handlerAddress; } + /** + @notice Sets or revokes fee whitelist from an address. + @param whitelistAddress Address to be whitelisted. + @param isWhitelisted Set to true to exempt an address from paying fees. + */ + function adminSetWhitelist(address whitelistAddress, bool isWhitelisted) external onlyAdmin { + _whitelist[whitelistAddress] = isWhitelisted; + + emit WhitelistChanged(whitelistAddress, isWhitelisted); + } + /** @notice Initiates collecting fee with corresponding fee handler contract using IFeeHandler interface. @@ -66,6 +82,11 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl { @param feeData Additional data to be passed to the fee handler. */ function collectFee(address sender, uint8 fromDomainID, uint8 destinationDomainID, bytes32 resourceID, bytes calldata depositData, bytes calldata feeData) payable external onlyBridge { + if (_whitelist[sender]) { + if (msg.value != 0) revert IncorrectFeeSupplied(msg.value); + return; + } + IFeeHandler feeHandler = _domainResourceIDToFeeHandlerAddress[destinationDomainID][resourceID]; feeHandler.collectFee{value: msg.value}(sender, fromDomainID, destinationDomainID, resourceID, depositData, feeData); } @@ -82,6 +103,10 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl { @return tokenAddress Returns the address of the token to be used for fee. */ function calculateFee(address sender, uint8 fromDomainID, uint8 destinationDomainID, bytes32 resourceID, bytes calldata depositData, bytes calldata feeData) external view returns(uint256 fee, address tokenAddress) { + if (_whitelist[sender]) { + return (0, address(0)); + } + IFeeHandler feeHandler = _domainResourceIDToFeeHandlerAddress[destinationDomainID][resourceID]; return feeHandler.calculateFee(sender, fromDomainID, destinationDomainID, resourceID, depositData, feeData); } diff --git a/test/feeRouter/feeRouter.js b/test/feeRouter/feeRouter.js deleted file mode 100644 index e69de29b..00000000 diff --git a/test/handlers/fee/handlerRouter.js b/test/handlers/fee/handlerRouter.js index 8c26a410..0b8f0f45 100644 --- a/test/handlers/fee/handlerRouter.js +++ b/test/handlers/fee/handlerRouter.js @@ -1,18 +1,25 @@ // The Licensed Work is (c) 2022 Sygma // SPDX-License-Identifier: LGPL-3.0-only +const Ethers = require("ethers"); + const TruffleAssert = require("truffle-assertions"); const Helpers = require("../../helpers"); -const DynamicFeeHandlerContract = artifacts.require("DynamicERC20FeeHandlerEVM"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); contract("FeeHandlerRouter", async (accounts) => { const originDomainID = 1; const destinationDomainID = 2; + const feeData = "0x0"; const nonAdmin = accounts[1]; + const whitelistAddress = accounts[2]; + const nonWhitelistAddress = accounts[3]; + const recipientAddress = accounts[3]; + const bridgeAddress = accounts[4]; const assertOnlyAdmin = (method, ...params) => { return TruffleAssert.reverts( @@ -21,27 +28,23 @@ contract("FeeHandlerRouter", async (accounts) => { ); }; - let BridgeInstance; let FeeHandlerRouterInstance; + let BasicFeeHandlerInstance; let ERC20MintableInstance; let resourceID; beforeEach(async () => { await Promise.all([ - (BridgeInstance = await Helpers.deployBridge( - destinationDomainID, - accounts[0] - )), ERC20MintableContract.new("token", "TOK").then( (instance) => (ERC20MintableInstance = instance) ), ]); FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( - BridgeInstance.address + bridgeAddress ); - DynamicFeeHandlerInstance = await DynamicFeeHandlerContract.new( - BridgeInstance.address, + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + bridgeAddress, FeeHandlerRouterInstance.address ); @@ -82,4 +85,144 @@ contract("FeeHandlerRouter", async (accounts) => { feeHandlerAddress ); }); + + it("should successfully set whitelist on an address", async () => { + assert.equal( + await FeeHandlerRouterInstance._whitelist.call( + whitelistAddress + ), + false + ); + + const whitelistTx = await FeeHandlerRouterInstance.adminSetWhitelist( + whitelistAddress, + true + ); + assert.equal( + await FeeHandlerRouterInstance._whitelist.call( + whitelistAddress + ), + true + ); + TruffleAssert.eventEmitted(whitelistTx, "WhitelistChanged", (event) => { + return ( + event.whitelistAddress === whitelistAddress && + event.isWhitelisted === true + ); + }); + }); + + it("should require admin role to set whitelist address", async () => { + await assertOnlyAdmin( + FeeHandlerRouterInstance.adminSetWhitelist, + whitelistAddress, + true + ); + }); + + it("should return fee 0 if address whitelisted", async () => { + await FeeHandlerRouterInstance.adminSetWhitelist( + whitelistAddress, + true + ); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + await BasicFeeHandlerInstance.changeFee(Ethers.utils.parseEther("0.5")); + + const depositData = Helpers.createERCDepositData(100, 20, recipientAddress); + let res = await FeeHandlerRouterInstance.calculateFee.call( + whitelistAddress, + originDomainID, + destinationDomainID, + resourceID, + depositData, + feeData + ); + assert.equal(web3.utils.fromWei(res[0], "ether"), "0") + res = await FeeHandlerRouterInstance.calculateFee.call( + nonWhitelistAddress, + originDomainID, + destinationDomainID, + resourceID, + depositData, + feeData + ); + assert.equal(web3.utils.fromWei(res[0], "ether"), "0.5") + }); + + it("should revert if whitelisted address provides fee", async () => { + await FeeHandlerRouterInstance.adminSetWhitelist( + whitelistAddress, + true + ); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + await BasicFeeHandlerInstance.changeFee(Ethers.utils.parseEther("0.5")); + + const depositData = Helpers.createERCDepositData(100, 20, recipientAddress); + await Helpers.expectToRevertWithCustomError( + FeeHandlerRouterInstance.collectFee( + whitelistAddress, + originDomainID, + destinationDomainID, + resourceID, + depositData, + feeData, + { + from: bridgeAddress, + value: Ethers.utils.parseEther("0.5").toString() + } + ), + "IncorrectFeeSupplied(uint256)" + ); + await TruffleAssert.passes( + FeeHandlerRouterInstance.collectFee( + nonWhitelistAddress, + originDomainID, + destinationDomainID, + resourceID, + depositData, + feeData, + { + from: bridgeAddress, + value: Ethers.utils.parseEther("0.5").toString() + } + ), + ); + }); + + it("should not collect fee from whitelisted address", async () => { + await FeeHandlerRouterInstance.adminSetWhitelist( + whitelistAddress, + true + ); + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID, + BasicFeeHandlerInstance.address + ); + await BasicFeeHandlerInstance.changeFee(Ethers.utils.parseEther("0.5")); + + const depositData = Helpers.createERCDepositData(100, 20, recipientAddress); + await TruffleAssert.passes( + FeeHandlerRouterInstance.collectFee( + whitelistAddress, + originDomainID, + destinationDomainID, + resourceID, + depositData, + feeData, + { + from: bridgeAddress, + value: "0" + } + ), + ); + }); });