Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Modified Dynamic Fee Handlers with twap oracle #236

Merged
merged 13 commits into from
May 2, 2024
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ jobs:
run: |
SILENT=true make start-ganache
make test
- name: Forked Mainnet Tests
run: |
fuser -k 8545/tcp
make start-forkedMainnet
npx truffle test testUnderForked/*

coverage:
needs: test
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ start-ganache:
@echo " > \033[32mStarting ganache... \033[0m "
./scripts/start_ganache.sh

start-forkedMainnet:
@echo " > \033[32mStarting forked environment... \033[0m "
ganache-cli -f https://eth-mainnet.g.alchemy.com/v2/34NZ4AoqM8OSolHSol6jh5xZSPq1rcL- & sleep 3

start-geth:
@echo " > \033[32mStarting geth... \033[0m "
./scripts/geth/start_geth.sh
Expand Down
32 changes: 32 additions & 0 deletions contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// The Licensed Work is (c) 2022 Sygma
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.11;

import "./DynamicFeeHandlerV2.sol";

/**
@title Handles deposit fees based on the destination chain's native coin price provided by Twap oracle.
@author ChainSafe Systems.
@notice This contract is intended to be used with the Bridge contract.
*/
contract DynamicERC20FeeHandlerEVMV2 is DynamicFeeHandlerV2 {

/**
@param bridgeAddress Contract address of previously deployed Bridge.
@param feeHandlerRouterAddress Contract address of previously deployed FeeHandlerRouter.
*/
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.
@return tokenAddress Returns the address of the token to be used for fee.
*/
function _calculateFee(address, uint8, uint8 destinationDomainID, bytes32, bytes calldata, bytes calldata) internal view override returns (uint256 fee, address tokenAddress) {
address desintationCoin = destinationNativeCoinWrap[destinationDomainID];
uint256 txCost = destinationGasPrice[destinationDomainID] * _gasUsed * twapOracle.getPrice(desintationCoin) / 1e18;
return (txCost, address(0));
}
}
166 changes: 166 additions & 0 deletions contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// The Licensed Work is (c) 2022 Sygma
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.11;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import "../../../interfaces/IFeeHandler.sol";
import "../../../interfaces/IERCHandler.sol";
import "../../../interfaces/IBridge.sol";
import "./TwapOracle.sol";

/**
@title Handles deposit fees based on Effective rates provided by Fee oracle.
@author ChainSafe Systems.
@notice This contract is intended to be used with the Bridge contract.
*/
abstract contract DynamicFeeHandlerV2 is IFeeHandler, AccessControl {
address public immutable _bridgeAddress;
address public immutable _feeHandlerRouterAddress;

TwapOracle public twapOracle;

uint32 public _gasUsed;

mapping(uint8 => address) public destinationNativeCoinWrap;
mapping(uint8 => uint256) public destinationGasPrice;

event FeeOracleAddressSet(TwapOracle feeOracleAddress);
event FeePropertySet(uint32 gasUsed);
event GasPriceSet(uint8 destinationDomainID, uint256 gasPrice);
event WrapTokenAddressSet(uint8 destinationDomainID, address wrapTokenAddress);

error IncorrectFeeSupplied(uint256);

modifier onlyAdmin() {
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "sender doesn't have admin role");
_;
}

modifier onlyBridgeOrRouter() {
_onlyBridgeOrRouter();
_;
}

function _onlyBridgeOrRouter() private view {
require(
msg.sender == _bridgeAddress || msg.sender == _feeHandlerRouterAddress,
"sender must be bridge or fee router contract"
);
}

/**
@param bridgeAddress Contract address of previously deployed Bridge.
@param feeHandlerRouterAddress Contract address of previously deployed FeeHandlerRouter.
*/
constructor(address bridgeAddress, address feeHandlerRouterAddress) {
_bridgeAddress = bridgeAddress;
_feeHandlerRouterAddress = feeHandlerRouterAddress;
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

// Admin functions

/**
@notice Removes admin role from {_msgSender()} and grants it to {newAdmin}.
@notice Only callable by an address that currently has the admin role.
@param newAdmin Address that admin role will be granted to.
*/
function renounceAdmin(address newAdmin) external {
address sender = _msgSender();
require(sender != newAdmin, 'Cannot renounce oneself');
grantRole(DEFAULT_ADMIN_ROLE, newAdmin);
renounceRole(DEFAULT_ADMIN_ROLE, sender);
}

/**
@notice Sets the fee oracle address for signature verification.
lastperson marked this conversation as resolved.
Show resolved Hide resolved
@param oracleAddress Fee oracle address.
*/
function setFeeOracle(TwapOracle oracleAddress) external onlyAdmin {
twapOracle = oracleAddress;
emit FeeOracleAddressSet(oracleAddress);
}

/**
@notice Sets the gas price for destination chain.
@param destinationDomainID ID of destination chain.
@param gasPrice Gas price of destination chain.
*/
function setGasPrice(uint8 destinationDomainID, uint256 gasPrice) external onlyAdmin {
destinationGasPrice[destinationDomainID] = gasPrice;
emit GasPriceSet(destinationDomainID, gasPrice);
}

/**
@notice Sets the wrap token address for destination chain.
@param destinationDomainID ID of destination chain.
@param wrapToken Wrap token address of destination chain.
*/
function setWrapTokenAddress(uint8 destinationDomainID, address wrapToken) external onlyAdmin {
destinationNativeCoinWrap[destinationDomainID] = wrapToken;
emit WrapTokenAddressSet(destinationDomainID, wrapToken);
}

/**
@notice Sets the fee properties.
@param gasUsed Gas used for transfer.
*/
function setFeeProperties(uint32 gasUsed) external onlyAdmin {
_gasUsed = gasUsed;
emit FeePropertySet(gasUsed);
}

/**
@notice Collects fee for deposit.
@param sender Sender of the deposit.
@param fromDomainID ID of the source chain.
@param destinationDomainID ID of chain deposit will be bridged to.
@param resourceID ResourceID to be used when making deposits.
@param depositData Additional data about the deposit.
@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 onlyBridgeOrRouter {
(uint256 fee, ) = _calculateFee(sender, fromDomainID, destinationDomainID, resourceID, depositData, feeData);
if (msg.value < fee) revert IncorrectFeeSupplied(msg.value);
uint256 remaining = msg.value - fee;
if (remaining != 0) {
(bool sent, ) = sender.call{value: remaining}("");
require(sent, "Failed to send remaining Ether");
}
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.
@param destinationDomainID ID of chain deposit will be bridged to.
@param resourceID ResourceID to be used when making deposits.
@param depositData Additional data about the deposit.
@param feeData Additional data to be passed to the fee handler.
@return fee Returns the fee amount.
@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) {
return _calculateFee(sender, fromDomainID, destinationDomainID, resourceID, depositData, feeData);
}

function _calculateFee(address sender, uint8 fromDomainID, uint8 destinationDomainID, bytes32 resourceID, bytes calldata depositData, bytes calldata feeData) internal view virtual returns(uint256 fee, address tokenAddress);

/**
@notice Transfers eth in the contract to the specified addresses. The parameters addrs and amounts are mapped 1-1.
This means that the address at index 0 for addrs will receive the amount (in WEI) from amounts at index 0.
@param addrs Array of addresses to transfer {amounts} to.
@param amounts Array of amounts to transfer to {addrs}.
*/
function transferFee(address payable[] calldata addrs, uint[] calldata amounts) external onlyAdmin {
require(addrs.length == amounts.length, "addrs[], amounts[]: diff length");
for (uint256 i = 0; i < addrs.length; i++) {
(bool success,) = addrs[i].call{value: amounts[i]}("");
require(success, "Fee ether transfer failed");
emit FeeDistributed(address(0), addrs[i], amounts[i]);
}
}
}
34 changes: 34 additions & 0 deletions contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// The Licensed Work is (c) 2022 Sygma
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.11;

import "./DynamicFeeHandlerV2.sol";

/**
@title Handles deposit fees based on the destination chain's native coin price provided by Twap oracle.
@author ChainSafe Systems.
@notice This contract is intended to be used with the Bridge contract.
*/
contract DynamicGenericFeeHandlerEVMV2 is DynamicFeeHandlerV2 {

/**
@param bridgeAddress Contract address of previously deployed Bridge.
@param feeHandlerRouterAddress Contract address of previously deployed FeeHandlerRouter.
*/
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.
@return fee Returns the fee amount.
@return tokenAddress Returns the address of the token to be used for fee.
*/
function _calculateFee(address, uint8, uint8 destinationDomainID, bytes32, bytes calldata depositData, bytes calldata) internal view override returns (uint256 fee, address tokenAddress) {
uint256 maxFee = uint256(bytes32(depositData[:32]));
address desintationCoin = destinationNativeCoinWrap[destinationDomainID];
uint256 txCost = destinationGasPrice[destinationDomainID] * maxFee * twapOracle.getPrice(desintationCoin) / 1e18;
return (txCost, address(0));
}
}
92 changes: 92 additions & 0 deletions contracts/handlers/fee/V2/TwapOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// The Licensed Work is (c) 2022 Sygma
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.11;

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol';
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import "../../../utils/TickMath.sol";
import "../../../utils/FullMath.sol";
import "../../../utils/AccessControl.sol";

contract TwapOracle is AccessControl {
IUniswapV3Factory public immutable UNISWAP_V3_FACTORY;
address public immutable WETH;

mapping(address => Pool) public pools;
mapping(address => uint256) public prices;

struct Pool {
address poolAddress;
uint32 timeWindow;
}

event PoolSet(address token, uint24 feeTier, uint32 timeWindow, address pool);
event PriceSet(address token, uint256 price);

error PairNotSupported();
error InvalidTimeWindow();
error InvalidPrice();
error UniswapPoolAvailable();

modifier onlyAdmin() {
_onlyAdmin();
_;
}

function _onlyAdmin() private view {
require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "sender doesn't have admin role");
}

constructor(IUniswapV3Factory _uniswapFactory, address _weth) {
UNISWAP_V3_FACTORY = _uniswapFactory;
WETH = _weth;
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

function getPrice(address quoteToken) external view returns (uint256 quotePrice) {
Pool memory pool = pools[quoteToken];
if (pool.poolAddress == address(0)) return prices[quoteToken];

uint32 secondsAgo = pool.timeWindow;
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = secondsAgo;
secondsAgos[1] = 0;

(int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool.poolAddress).observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(secondsAgo)));
// Always round to negative infinity
if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int56(uint56(secondsAgo)) != 0)) arithmeticMeanTick--;

uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);

// Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself
if (sqrtRatioX96 <= type(uint128).max) {
uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
quotePrice = quoteToken < WETH
? FullMath.mulDiv(ratioX192, 1e18, 1 << 192)
: FullMath.mulDiv(1 << 192, 1e18, ratioX192);
} else {
uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
quotePrice = quoteToken < WETH
? FullMath.mulDiv(ratioX128, 1e18, 1 << 128)
: FullMath.mulDiv(1 << 128, 1e18, ratioX128);
}
return quotePrice;
}

function setPool(address token, uint24 feeTier, uint32 timeWindow) external onlyAdmin {
if (timeWindow == 0) revert InvalidTimeWindow();
address _pool = UNISWAP_V3_FACTORY.getPool(WETH, token, feeTier);
if (!Address.isContract(_pool)) revert PairNotSupported();
pools[token].poolAddress = _pool;
pools[token].timeWindow = timeWindow;
emit PoolSet(token, feeTier, timeWindow, _pool);
}

function setPrice(address token, uint256 price) external onlyAdmin {
prices[token] = price;
delete pools[token];
emit PriceSet(token, price);
}
}
Loading
Loading