Skip to content

Commit

Permalink
feat: Modified Dynamic Fee Handlers with twap oracle (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
andersonlee725 authored May 2, 2024
1 parent 824151a commit 45ad376
Show file tree
Hide file tree
Showing 14 changed files with 1,673 additions and 2 deletions.
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.
@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

0 comments on commit 45ad376

Please sign in to comment.