diff --git a/contracts/agToken/TokenSideChainMultiBridge.sol b/contracts/agToken/TokenSideChainMultiBridge.sol new file mode 100644 index 00000000..067548fa --- /dev/null +++ b/contracts/agToken/TokenSideChainMultiBridge.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; + +import "../interfaces/ICoreBorrow.sol"; + +/// @title TokenSideChainMultiBridge +/// @author Angle Labs, Inc. +/// @notice Contract for an ERC20 token that exists natively on a chain on other chains than this native chain +/// @dev This contract supports bridge tokens having a minting right on the ERC20 token (also referred to as the canonical +/// of the chain) +/// @dev This implementation assumes that the token bridged has 18 decimals +contract TokenSideChainMultiBridge is ERC20PermitUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Struct with some data about a specific bridge token + struct BridgeDetails { + // Limit on the balance of bridge token held by the contract: it is designed + // to reduce the exposure of the system to hacks + uint256 limit; + // Limit on the hourly volume of token minted through this bridge + // Technically the limit over a rolling hour is hourlyLimit x2 as hourly limit + // is enforced only between x:00 and x+1:00 + uint256 hourlyLimit; + // Fee taken for swapping in and out the token + uint64 fee; + // Whether the associated token is allowed or not + bool allowed; + // Whether swapping in and out from the associated token is paused or not + bool paused; + } + + /// @notice Base used for fee computation + uint256 public constant BASE_PARAMS = 10 ** 9; + + // ================================= PARAMETER ================================= + + /// @notice Reference to the core contract which handles access control + ICoreBorrow public core; + + // =============================== BRIDGING DATA =============================== + + /// @notice Maps a bridge token to data + mapping(address => BridgeDetails) public bridges; + /// @notice List of all bridge tokens + address[] public bridgeTokensList; + /// @notice Maps a bridge token to the associated hourly volume + mapping(address => mapping(uint256 => uint256)) public usage; + /// @notice Maps an address to whether it is exempt of fees for when it comes to swapping in and out + mapping(address => uint256) public isFeeExempt; + /// @notice Limit to the amount of tokens that can be sent from that chain to another chain + uint256 public chainTotalHourlyLimit; + /// @notice Usage per hour on that chain. Maps an hourly timestamp to the total volume swapped out on the chain + mapping(uint256 => uint256) public chainTotalUsage; + + // =================================== ERRORS ================================== + + error AssetStillControlledInReserves(); + error HourlyLimitExceeded(); + error InvalidToken(); + error NotGovernor(); + error NotGovernorOrGuardian(); + error TooHighParameterValue(); + error ZeroAddress(); + + // =================================== EVENTS ================================== + + event BridgeTokenAdded(address indexed bridgeToken, uint256 limit, uint256 hourlyLimit, uint64 fee, bool paused); + event BridgeTokenToggled(address indexed bridgeToken, bool toggleStatus); + event BridgeTokenRemoved(address indexed bridgeToken); + event BridgeTokenFeeUpdated(address indexed bridgeToken, uint64 fee); + event BridgeTokenLimitUpdated(address indexed bridgeToken, uint256 limit); + event BridgeTokenHourlyLimitUpdated(address indexed bridgeToken, uint256 hourlyLimit); + event HourlyLimitUpdated(uint256 hourlyLimit); + event Recovered(address indexed token, address indexed to, uint256 amount); + event FeeToggled(address indexed theAddress, uint256 toggleStatus); + + // ================================= MODIFIERS ================================= + + /// @notice Checks whether the `msg.sender` has the governor role or not + modifier onlyGovernor() { + if (!core.isGovernor(msg.sender)) revert NotGovernor(); + _; + } + + /// @notice Checks whether the `msg.sender` has the governor role or the guardian role + modifier onlyGovernorOrGuardian() { + if (!core.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); + _; + } + + // ================================ CONSTRUCTOR ================================ + + /// @notice Initializes the contract + /// @param name_ Name of the token + /// @param symbol_ Symbol of the token + /// @param _core Reference to the `CoreBorrow` contract + /// @dev This implementation allows to add directly at deployment a bridge token + function initialize( + string memory name_, + string memory symbol_, + ICoreBorrow _core, + address bridgeToken, + uint256 limit, + uint256 hourlyLimit, + uint64 fee, + bool paused, + uint256 _chainTotalHourlyLimit + ) external initializer { + __ERC20Permit_init(name_); + __ERC20_init(name_, symbol_); + if (address(_core) == address(0)) revert ZeroAddress(); + core = _core; + _addBridgeToken(bridgeToken, limit, hourlyLimit, fee, paused); + _setChainTotalHourlyLimit(_chainTotalHourlyLimit); + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() initializer {} + + // ===================== EXTERNAL PERMISSIONLESS FUNCTIONS ===================== + + /// @notice Returns the list of all supported bridge tokens + /// @dev Helpful for UIs + function allBridgeTokens() external view returns (address[] memory) { + return bridgeTokensList; + } + + /// @notice Returns the current volume for a bridge, for the current hour + /// @param bridgeToken Bridge used to mint + /// @dev Helpful for UIs + function currentUsage(address bridgeToken) external view returns (uint256) { + return usage[bridgeToken][block.timestamp / 3600]; + } + + /// @notice Returns the current total volume on the chain for the current hour + /// @dev Helpful for UIs + function currentTotalUsage() external view returns (uint256) { + return chainTotalUsage[block.timestamp / 3600]; + } + + /// @notice Mints the canonical token from a supported bridge token + /// @param bridgeToken Bridge token to use to mint + /// @param amount Amount of bridge tokens to send + /// @param to Address to which the token should be sent + /// @return Amount of canonical token actually minted + /// @dev Some fees may be taken by the protocol depending on the token used and on the address calling + function swapIn(address bridgeToken, uint256 amount, address to) external returns (uint256) { + BridgeDetails memory bridgeDetails = bridges[bridgeToken]; + if (!bridgeDetails.allowed || bridgeDetails.paused) revert InvalidToken(); + uint256 balance = IERC20(bridgeToken).balanceOf(address(this)); + if (balance + amount > bridgeDetails.limit) { + // In case someone maliciously sends tokens to this contract + // Or the limit changes + if (bridgeDetails.limit > balance) amount = bridgeDetails.limit - balance; + else { + amount = 0; + } + } + + // Checking requirement on the hourly volume + uint256 hour = block.timestamp / 3600; + uint256 hourlyUsage = usage[bridgeToken][hour]; + if (hourlyUsage + amount > bridgeDetails.hourlyLimit) { + // Edge case when the hourly limit changes + amount = bridgeDetails.hourlyLimit > hourlyUsage ? bridgeDetails.hourlyLimit - hourlyUsage : 0; + } + usage[bridgeToken][hour] = hourlyUsage + amount; + + IERC20(bridgeToken).safeTransferFrom(msg.sender, address(this), amount); + uint256 canonicalOut = amount; + // Computing fees + if (isFeeExempt[msg.sender] == 0) { + canonicalOut -= (canonicalOut * bridgeDetails.fee) / BASE_PARAMS; + } + _mint(to, canonicalOut); + return canonicalOut; + } + + /// @notice Burns the canonical token in exchange for a bridge token + /// @param bridgeToken Bridge token required + /// @param amount Amount of canonical tokens to burn + /// @param to Address to which the bridge token should be sent + /// @return Amount of bridge tokens actually sent back + /// @dev Some fees may be taken by the protocol depending on the token used and on the address calling + function swapOut(address bridgeToken, uint256 amount, address to) external returns (uint256) { + BridgeDetails memory bridgeDetails = bridges[bridgeToken]; + if (!bridgeDetails.allowed || bridgeDetails.paused) revert InvalidToken(); + + uint256 hour = block.timestamp / 3600; + uint256 hourlyUsage = chainTotalUsage[hour] + amount; + // If the amount being swapped out exceeds the limit, we revert + // We don't want to change the amount being swapped out. + // The user can decide to send another tx with the correct amount to reach the limit + if (hourlyUsage > chainTotalHourlyLimit) revert HourlyLimitExceeded(); + chainTotalUsage[hour] = hourlyUsage; + + _burn(msg.sender, amount); + uint256 bridgeOut = amount; + if (isFeeExempt[msg.sender] == 0) { + bridgeOut -= (bridgeOut * bridgeDetails.fee) / BASE_PARAMS; + } + IERC20(bridgeToken).safeTransfer(to, bridgeOut); + return bridgeOut; + } + + // ============================ GOVERNANCE FUNCTIONS =========================== + + /// @notice Sets a new `core` contract + /// @dev One sanity check that can be performed here is to verify whether at least the governor + /// calling the contract is still a governor in the new core + function setCore(ICoreBorrow _core) external onlyGovernor { + if (!_core.isGovernor(msg.sender)) revert NotGovernor(); + core = _core; + } + + /// @notice Adds support for a bridge token + /// @param bridgeToken Bridge token to add: it should be a version of the canonical token from another bridge + /// @param limit Limit on the balance of bridge token this contract could hold + /// @param hourlyLimit Limit on the hourly volume for this bridge + /// @param paused Whether swapping for this token should be paused or not + /// @param fee Fee taken upon swapping for or against this token + function addBridgeToken( + address bridgeToken, + uint256 limit, + uint256 hourlyLimit, + uint64 fee, + bool paused + ) external onlyGovernor { + _addBridgeToken(bridgeToken, limit, hourlyLimit, fee, paused); + } + + /// @notice Removes support for a token + /// @param bridgeToken Address of the bridge token to remove support for + function removeBridgeToken(address bridgeToken) external onlyGovernor { + if (IERC20(bridgeToken).balanceOf(address(this)) != 0) revert AssetStillControlledInReserves(); + delete bridges[bridgeToken]; + // Deletion from `bridgeTokensList` loop + uint256 bridgeTokensListLength = bridgeTokensList.length; + for (uint256 i = 0; i < bridgeTokensListLength - 1; i++) { + if (bridgeTokensList[i] == bridgeToken) { + // Replace the `bridgeToken` to remove with the last of the list + bridgeTokensList[i] = bridgeTokensList[bridgeTokensListLength - 1]; + break; + } + } + // Remove last element in array + bridgeTokensList.pop(); + emit BridgeTokenRemoved(bridgeToken); + } + + /// @notice Recovers any ERC20 token + /// @dev Can be used to withdraw bridge tokens for them to be de-bridged on mainnet + function recoverERC20(address tokenAddress, address to, uint256 amountToRecover) external onlyGovernor { + IERC20(tokenAddress).safeTransfer(to, amountToRecover); + emit Recovered(tokenAddress, to, amountToRecover); + } + + /// @notice Updates the `limit` amount for `bridgeToken` + function setLimit(address bridgeToken, uint256 limit) external onlyGovernorOrGuardian { + if (!bridges[bridgeToken].allowed) revert InvalidToken(); + bridges[bridgeToken].limit = limit; + emit BridgeTokenLimitUpdated(bridgeToken, limit); + } + + /// @notice Updates the `hourlyLimit` amount for `bridgeToken` + function setHourlyLimit(address bridgeToken, uint256 hourlyLimit) external onlyGovernorOrGuardian { + if (!bridges[bridgeToken].allowed) revert InvalidToken(); + bridges[bridgeToken].hourlyLimit = hourlyLimit; + emit BridgeTokenHourlyLimitUpdated(bridgeToken, hourlyLimit); + } + + /// @notice Updates the `chainTotalHourlyLimit` amount + function setChainTotalHourlyLimit(uint256 hourlyLimit) external onlyGovernorOrGuardian { + _setChainTotalHourlyLimit(hourlyLimit); + } + + /// @notice Updates the `fee` value for `bridgeToken` + function setSwapFee(address bridgeToken, uint64 fee) external onlyGovernorOrGuardian { + if (!bridges[bridgeToken].allowed) revert InvalidToken(); + if (fee > BASE_PARAMS) revert TooHighParameterValue(); + bridges[bridgeToken].fee = fee; + emit BridgeTokenFeeUpdated(bridgeToken, fee); + } + + /// @notice Pauses or unpauses swapping in and out for a token + function toggleBridge(address bridgeToken) external onlyGovernorOrGuardian { + if (!bridges[bridgeToken].allowed) revert InvalidToken(); + bool pausedStatus = bridges[bridgeToken].paused; + bridges[bridgeToken].paused = !pausedStatus; + emit BridgeTokenToggled(bridgeToken, !pausedStatus); + } + + /// @notice Toggles fees for the address `theAddress` + function toggleFeesForAddress(address theAddress) external onlyGovernorOrGuardian { + uint256 feeExemptStatus = 1 - isFeeExempt[theAddress]; + isFeeExempt[theAddress] = feeExemptStatus; + emit FeeToggled(theAddress, feeExemptStatus); + } + + // ============================= INTERNAL FUNCTIONS ============================ + + /// @notice Internal version of the `addBridgeToken` function + function _addBridgeToken( + address bridgeToken, + uint256 limit, + uint256 hourlyLimit, + uint64 fee, + bool paused + ) internal { + if (bridges[bridgeToken].allowed || bridgeToken == address(0)) revert InvalidToken(); + if (fee > BASE_PARAMS) revert TooHighParameterValue(); + BridgeDetails memory _bridge; + _bridge.limit = limit; + _bridge.hourlyLimit = hourlyLimit; + _bridge.paused = paused; + _bridge.fee = fee; + _bridge.allowed = true; + bridges[bridgeToken] = _bridge; + bridgeTokensList.push(bridgeToken); + emit BridgeTokenAdded(bridgeToken, limit, hourlyLimit, fee, paused); + } + + /// @notice Internal version of the `setChainTotalHourlyLimit` + function _setChainTotalHourlyLimit(uint256 hourlyLimit) internal { + chainTotalHourlyLimit = hourlyLimit; + emit HourlyLimitUpdated(hourlyLimit); + } +} diff --git a/deploy/constants/constants.ts b/deploy/constants/constants.ts index 2653fed7..b51cdec3 100644 --- a/deploy/constants/constants.ts +++ b/deploy/constants/constants.ts @@ -40,6 +40,13 @@ export const OFTs: OFTsStructure = { polygon: '0xe70575daaB2B1b3fa9658fa76cC506fcB0007169', polygonzkevm: '0x1E5B48c08D6b5efE0792d04f27602bD90026514a', }, + ANGLE: { + mainnet: '0x1056178977457A5F4BE33929520455A7d2E28670', + optimism: '0x9201cC18965792808549566e6B06B016d915313A', + arbitrum: '0x366CEE609A64037a4910868c5b3cd62b9D019695', + bsc: '0x16cd38b1B54E7abf307Cb2697E2D9321e843d5AA', + avalanche: '0xC011882d0f7672D8942e7fE2248C174eeD640c8f' + } }; interface CurrencyNetworkAddresses { diff --git a/lib/utils b/lib/utils index e33f4b9f..41388c6e 160000 --- a/lib/utils +++ b/lib/utils @@ -1 +1 @@ -Subproject commit e33f4b9f042704eb02f37e32d93c108f5005cd3f +Subproject commit 41388c6ea9c45b05b92155356d6e700802fdb566