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: Angle crosschain #252

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 333 additions & 0 deletions contracts/agToken/TokenSideChainMultiBridge.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Comment on lines +258 to +261

Check notice

Code scanning / Slither

Reentrancy vulnerabilities Low


/// @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;

Check warning

Code scanning / Slither

Uninitialized local variables Medium

_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);
}
}
Comment on lines +17 to +333

Check warning

Code scanning / Slither

Missing inheritance Warning

7 changes: 7 additions & 0 deletions deploy/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion lib/utils
Loading