Skip to content

Commit

Permalink
feat: extend fee calculation formula (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmlinaric authored Jun 3, 2024
1 parent ee0954f commit cfc09d8
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
2 changes: 1 addition & 1 deletion contracts/handlers/FeeHandlerRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));
}
}
29 changes: 25 additions & 4 deletions contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));
}
}
61 changes: 54 additions & 7 deletions testUnderForked/calculateFeeERC20EVM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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
);
});
});
15 changes: 13 additions & 2 deletions testUnderForked/collectFeeERC20EVM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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(
Expand Down
15 changes: 13 additions & 2 deletions testUnderForked/collectFeeGenericEVM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit cfc09d8

Please sign in to comment.