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
37 changes: 37 additions & 0 deletions contracts/handlers/fee/V2/DynamicERC20FeeHandlerEVMV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 for generic messages based on Effective rates provided by Fee 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 sender Sender of the deposit. // Not used
@param fromDomainID ID of the source chain. // Not used
@param destinationDomainID ID of chain deposit will be bridged to.
@param resourceID ResourceID to be used when making deposits. // Not used
@param depositData Additional data to be passed to specified handler.
@param feeData Additional data about the deposit. // Not used
@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) 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));
}
}
162 changes: 162 additions & 0 deletions contracts/handlers/fee/V2/DynamicFeeHandlerV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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);
lastperson marked this conversation as resolved.
Show resolved Hide resolved
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) {
}
lastperson marked this conversation as resolved.
Show resolved Hide resolved

/**
@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]);
}
}
}
38 changes: 38 additions & 0 deletions contracts/handlers/fee/V2/DynamicGenericFeeHandlerEVMV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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 for generic messages based on Effective rates provided by Fee oracle.
lastperson marked this conversation as resolved.
Show resolved Hide resolved
@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 sender Sender of the deposit. // Not used
@param fromDomainID ID of the source chain. // Not used
@param destinationDomainID ID of chain deposit will be bridged to.
@param resourceID ResourceID to be used when making deposits. // Not used
@param depositData Additional data to be passed to specified handler. // Not used
@param feeData Additional data about the deposit. // Not used
@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) internal view override returns (uint256 fee, address tokenAddress) {
lastperson marked this conversation as resolved.
Show resolved Hide resolved
uint256 maxFee = uint256(bytes32(depositData[:32]));
address desintationCoin = destinationNativeCoinWrap[destinationDomainID];
uint256 txCost = destinationGasPrice[destinationDomainID] * maxFee * twapOracle.getPrice(desintationCoin) / 1e18;
return (txCost, address(0));
}
}
112 changes: 112 additions & 0 deletions contracts/handlers/fee/V2/TwapOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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/PoolAddress.sol";
import "../../../utils/AccessControl.sol";

contract TwapOracle is AccessControl {
IUniswapV3Factory public immutable UNISWAP_V3_FACTORY;
address public immutable WETH;
uint24[] internal _knownFeeTiers;
lastperson marked this conversation as resolved.
Show resolved Hide resolved

uint32 internal _timeWindow;
lastperson marked this conversation as resolved.
Show resolved Hide resolved
mapping(address => mapping(address => uint24)) public feeTiers;

event TimeWindowUpdated(uint32 timeWindow);
event FeeTierAdded(uint24 feeTier);
event FeeTierSet(address tokenA, address tokenB, uint24 feeTier);

error PairNotSupported();
error FeeTierNotSupported();
error FeeTierAlreadySupported();
error InvalidTimeWindow();

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

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

constructor(IUniswapV3Factory _uniswapFactory, address _weth, uint32 timeWindow) {
if (timeWindow == 0) revert InvalidTimeWindow();
UNISWAP_V3_FACTORY = _uniswapFactory;
WETH = _weth;
_timeWindow = timeWindow;
viatrix marked this conversation as resolved.
Show resolved Hide resolved
_knownFeeTiers.push(500);
_knownFeeTiers.push(3000);
_knownFeeTiers.push(10000);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

function isFeeTierSupported(uint24 feeTier) public view returns (bool) {
uint256 length = _knownFeeTiers.length;
for (uint256 i; i < length; i++) {
if (_knownFeeTiers[i] == feeTier) return true;
}
return false;
}

function getPrice(address quoteToken) external view returns (uint256 quotePrice) {
address _pool = PoolAddress.computeAddress(address(UNISWAP_V3_FACTORY), PoolAddress.getPoolKey(WETH, quoteToken, feeTiers[WETH][quoteToken]));
viatrix marked this conversation as resolved.
Show resolved Hide resolved
if (!Address.isContract(_pool)) revert PairNotSupported();

uint32 secondsAgo = _timeWindow;
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = secondsAgo;
secondsAgos[1] = 0;

(int56[] memory tickCumulatives, ) = IUniswapV3Pool(_pool).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 updateTimeWindow(uint32 timeWindow) external onlyAdmin {
if (timeWindow == 0) revert InvalidTimeWindow();
_timeWindow = timeWindow;
viatrix marked this conversation as resolved.
Show resolved Hide resolved
emit TimeWindowUpdated(timeWindow);
}

function addNewFeeTier(uint24 feeTier) external onlyAdmin {
uint256 length = _knownFeeTiers.length;
for (uint256 i; i < length; i++) {
if (_knownFeeTiers[i] == feeTier) revert FeeTierAlreadySupported();
}
_knownFeeTiers.push(feeTier);
emit FeeTierAdded(feeTier);
}

function setFeeTier(address tokenA, address tokenB, uint24 feeTier) external onlyAdmin {
if (!isFeeTierSupported(feeTier)) revert FeeTierNotSupported();
viatrix marked this conversation as resolved.
Show resolved Hide resolved
address _pool = PoolAddress.computeAddress(address(UNISWAP_V3_FACTORY), PoolAddress.getPoolKey(tokenA, tokenB, feeTier));
if (!Address.isContract(_pool)) revert PairNotSupported();
feeTiers[tokenA][tokenB] = feeTier;
feeTiers[tokenB][tokenA] = feeTier;
emit FeeTierSet(tokenA, tokenB, feeTier);
}
}
Loading
Loading