-
Notifications
You must be signed in to change notification settings - Fork 15
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
sogipec
wants to merge
2
commits into
main
Choose a base branch
from
angle-crosschain
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
/// @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
TokenSideChainMultiBridge._addBridgeToken(address,uint256,uint256,uint64,bool)._bridge is a local variable never initialized
|
||
_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
TokenSideChainMultiBridge should inherit from IAgTokenSideChainMultiBridge
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule utils
updated
5 files
+1 −0 | .npmrc | |
+6 −0 | helpers/common.sh | |
+2 −2 | package.json | |
+10 −10 | utils/contractAddress.js | |
+161 −6 | yarn.lock |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check notice
Code scanning / Slither
Reentrancy vulnerabilities Low