Skip to content

Commit

Permalink
Merge pull request #41 from immutable/SMR-1677-FlowRate
Browse files Browse the repository at this point in the history
SMR-1677 Flow Rate
  • Loading branch information
proletesseract authored Nov 19, 2023
2 parents bf38d42 + 0d2f16e commit a5a9f06
Show file tree
Hide file tree
Showing 14 changed files with 2,296 additions and 2,001 deletions.
25 changes: 12 additions & 13 deletions src/interfaces/root/IRootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,6 @@ interface IRootERC20Bridge {
*/
function mapToken(IERC20Metadata rootToken) external payable returns (address);

/**
* @notice Deposits `amount` of ETH to `msg.sender` on the child chain.
* @param amount The amount of ETH to deposit.
*/
function depositETH(uint256 amount) external payable;

/**
* @notice Deposits `amount` of ETH to `receiver` on the child chain.
* @param receiver The address to deposit the ETH to.
* @param amount The amount of ETH to deposit.
*/
function depositToETH(address receiver, uint256 amount) external payable;

/**
* @notice Initiate sending a deposit message to the child chain.
* @custom:requires `rootToken` to already be mapped with `mapToken`.
Expand All @@ -71,6 +58,18 @@ interface IRootERC20Bridge {
* @param amount The amount of tokens to deposit.
*/
function depositTo(IERC20Metadata rootToken, address receiver, uint256 amount) external payable;

/**
* @notice Initiate sending an ETH deposit message to the child chain.
* @param amount The amount of tokens to deposit.
*/
function depositETH(uint256 amount) external payable;
/**
* @notice Initiate sending an ETH deposit message to the child chain, with a specified receiver.
* @param receiver The address of the receiver on the child chain.
* @param amount The amount of tokens to deposit.
*/
function depositToETH(address receiver, uint256 amount) external payable;
}

interface IRootERC20BridgeEvents {
Expand Down
42 changes: 42 additions & 0 deletions src/interfaces/root/flowrate/IFlowRateWithdrawalQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

interface IFlowRateWithdrawalQueueEvents {
// Indicates a withdrawal has been queued.
event EnQueuedWithdrawal(
address indexed token,
address indexed withdrawer,
address indexed receiver,
uint256 amount,
uint256 timestamp,
uint256 index
);

// Indicates a withdrawal has been processed.
event ProcessedWithdrawal(
address indexed token,
address indexed withdrawer,
address indexed receiver,
uint256 amount,
uint256 timestamp,
uint256 index
);

// Indicates the new withdrawal delay.
event WithdrawalDelayUpdated(uint256 delay, uint256 previousDelay);
}

interface IFlowRateWithdrawalQueueErrors {
// A withdrawal was being processed, but the index is outside of the array.
error IndexOutsideWithdrawalQueue(uint256 lengthOfQueue, uint256 requestedIndex);

// A withdrawal was being processed, but the withdrawal is not yet available.
error WithdrawalRequestTooEarly(uint256 timeNow, uint256 currentWithdrawalTime);

// A withdrawal was being processed, but the token is zero. This indicates that the
// withdrawal has already been processed.
error WithdrawalAlreadyProcessed(address receiver, uint256 index);

// Attempting to enqueue a token with token address = 0.
error TokenIsZero(address receiver);
}
52 changes: 52 additions & 0 deletions src/interfaces/root/flowrate/IRootERC20BridgeFlowRate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

interface IRootERC20BridgeFlowRateEvents {
/**
* @notice Indicates rate control thresholds have been set for a certain token.
* @param token The token thresholds applied to.
* @param capacity The size of the bucket in tokens.
* @param refillRate How quickly the bucket refills in tokens per second.
* @param largeTransferThreshold Threshold over which a withdrawal is deemed to be large,
* and will be put in the withdrawal queue.
*/
event RateControlThresholdSet(
address indexed token,
uint256 capacity,
uint256 refillRate,
uint256 largeTransferThreshold,
uint256 previousCapacity,
uint256 previousRefillRate,
uint256 previousLargeTransferThreshold
);

/**
* @notice Indicates a withdrawal was queued.
* @param token Address of token that is being withdrawn.
* @param withdrawer Child chain sender of tokens.
* @param receiver Recipient of tokens.
* @param amount The number of tokens.
* @param delayWithdrawalLargeAmount is true if the reason for queuing was a large transfer.
* @param delayWithdrawalUnknownToken is true if the reason for queuing was that the
* token had not been configured using the setRateControlThreshold function.
* @param withdrawalQueueActivated is true if the withdrawal queue has been activated.
*/
event QueuedWithdrawal(
address indexed token,
address indexed withdrawer,
address indexed receiver,
uint256 amount,
bool delayWithdrawalLargeAmount,
bool delayWithdrawalUnknownToken,
bool withdrawalQueueActivated
);
}

interface IRootERC20BridgeFlowRateErrors {
// Error if the RootERC20Bridge initializer is called, and not the one for this contract.
error WrongInitializer();
// finaliseQueuedWithdrawalsAggregated was called with a zero length indices array.
error ProvideAtLeastOneIndex();
// The expected and actual token did not match for an aggregated withdrawal.
error MixedTokens(address token, address actualToken);
}
63 changes: 57 additions & 6 deletions src/root/RootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {IAxelarGateway} from "@axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol";
import {
IRootERC20Bridge,
IERC20Metadata,
Expand Down Expand Up @@ -83,7 +86,43 @@ contract RootERC20Bridge is IRootERC20Bridge, IRootERC20BridgeEvents, IRootERC20
address newRootWETHToken,
string memory newChildChain,
uint256 newImxCumulativeDepositLimit
) public initializer {
) external virtual initializer {
__RootERC20Bridge_init(
newRoles,
newRootBridgeAdaptor,
newChildERC20Bridge,
newChildBridgeAdaptor,
newChildTokenTemplate,
newRootIMXToken,
newRootWETHToken,
newChildChain,
newImxCumulativeDepositLimit
);
}

/**
* @notice Initialization function for RootERC20Bridge.
* @param newRoles Struct containing addresses of roles.
* @param newRootBridgeAdaptor Address of StateSender to send bridge messages to, and receive messages from.
* @param newChildERC20Bridge Address of child ERC20 bridge to communicate with.
* @param newChildBridgeAdaptor Address of child bridge adaptor to communicate with (As a checksummed string).
* @param newChildTokenTemplate Address of child token template to clone.
* @param newRootIMXToken Address of ERC20 IMX on the root chain.
* @param newRootWETHToken Address of ERC20 WETH on the root chain.
* @param newChildChain Name of child chain.
* @param newImxCumulativeDepositLimit The cumulative IMX deposit limit.
*/
function __RootERC20Bridge_init(
InitializationRoles memory newRoles,
address newRootBridgeAdaptor,
address newChildERC20Bridge,
string memory newChildBridgeAdaptor,
address newChildTokenTemplate,
address newRootIMXToken,
address newRootWETHToken,
string memory newChildChain,
uint256 newImxCumulativeDepositLimit
) internal {
if (
newRootBridgeAdaptor == address(0) || newChildERC20Bridge == address(0)
|| newChildTokenTemplate == address(0) || newRootIMXToken == address(0) || newRootWETHToken == address(0)
Expand Down Expand Up @@ -397,10 +436,23 @@ contract RootERC20Bridge is IRootERC20Bridge, IRootERC20BridgeEvents, IRootERC20
}
}

function _withdraw(bytes memory data) private {
(address rootToken, address withdrawer, address receiver, uint256 amount) =
abi.decode(data, (address, address, address, uint256));
address childToken;
function _withdraw(bytes memory data) internal virtual {
(address rootToken, address childToken, address withdrawer, address receiver, uint256 amount) =
_decodeAndValidateWithdrawal(data);
_executeTransfer(rootToken, childToken, withdrawer, receiver, amount);
}

function _decodeAndValidateWithdrawal(bytes memory data)
internal
view
returns (address rootToken, address childToken, address withdrawer, address receiver, uint256 amount)
{
(rootToken, withdrawer, receiver, amount) = abi.decode(data, (address, address, address, uint256));

if (address(rootToken) == address(0)) {
revert ZeroAddress();
}

if (rootToken == rootIMXToken) {
childToken = NATIVE_IMX;
} else if (rootToken == NATIVE_ETH) {
Expand All @@ -411,7 +463,6 @@ contract RootERC20Bridge is IRootERC20Bridge, IRootERC20BridgeEvents, IRootERC20
revert NotMapped();
}
}
_executeTransfer(rootToken, childToken, withdrawer, receiver, amount);
}

function _executeTransfer(
Expand Down
130 changes: 130 additions & 0 deletions src/root/flowrate/FlowRateDetection.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

/**
* @title Flow Rate Detection
* @author Immutable Pty Ltd (Peter Robinson @drinkcoffee and Zhenyang Shi @wcgcyx)
* @notice Detects large flows of tokens using a bucket system.
* @dev Each token has a bucket. The bucket is filled at a constant rate: a number of
* tokens per second. The bucket empties each time there is a withdrawal. Withdrawal
* requests for tokens that don't have a configured bucket are delayed.
* Note: This code is part of RootERC20BridgeFlowRate. It has been separated out
* to make it easier to understand and test the functionality.
* Note that this contract is upgradeable.
*/
abstract contract FlowRateDetection {
// Holds flow rate information for a single token.
struct Bucket {
// The number of tokens that can fit in the bucket.
// A capacity of zero indicates that flow rate detection is not configured for the token.
uint256 capacity;
// The number of tokens in the bucket.
uint256 depth;
// The last time the bucket was updated.
uint256 refillTime;
// The number of tokens added per second.
uint256 refillRate;
}

// Map ERC 20 token address to buckets
mapping(address => Bucket) public flowRateBuckets;

// True if all tokens should be put in the withdrawal queue.
bool public withdrawalQueueActivated;

// Emitted when there is a withdrawal request for a token for which there is no bucket.
event WithdrawalForNonFlowRatedToken(address indexed token, uint256 amount);
// Emitted when queue activated or deactivated
event AutoActivatedWithdrawalQueue();
event ActivatedWithdrawalQueue(address who);
event DeactivatedWithdrawalQueue(address who);

error InvalidToken();
error InvalidCapacity();
error InvalidRefillRate();

/**
* @notice Activate the withdrawal queue for all tokens.
*/
function _activateWithdrawalQueue() internal {
withdrawalQueueActivated = true;
emit ActivatedWithdrawalQueue(msg.sender);
}

/**
* @notice Deactivate the withdrawal queue for all tokens.
* @dev This does not affect withdrawals already in the queue.
*/
function _deactivateWithdrawalQueue() internal {
withdrawalQueueActivated = false;
emit DeactivatedWithdrawalQueue(msg.sender);
}

/**
* @notice Initialise or update a bucket for a token.
* @param token Address of the token to configure the bucket for.
* @param capacity The number of tokens before the bucket overflows.
* @param refillRate The number of tokens added to the bucket each second.
* @dev If this is a new bucket, then the depth is the capacity. If the bucket is existing, then
* the depth is not altered.
*/
function _setFlowRateThreshold(address token, uint256 capacity, uint256 refillRate) internal {
if (token == address(0)) {
revert InvalidToken();
}
if (capacity == 0) {
revert InvalidCapacity();
}
if (refillRate == 0) {
revert InvalidRefillRate();
}
Bucket storage bucket = flowRateBuckets[token];
if (bucket.capacity == 0) {
bucket.depth = capacity;
}
bucket.capacity = capacity;
bucket.refillRate = refillRate;
}

/**
* @notice Update the flow rate measurement for a token.
* @param token Address of token being withdrawn.
* @param amount The number of tokens being withdrawn.
* @return delayWithdrawal Delay this withdrawal because it is for an unconfigured token.
*/
function _updateFlowRateBucket(address token, uint256 amount) internal returns (bool delayWithdrawal) {
Bucket storage bucket = flowRateBuckets[token];

uint256 capacity = bucket.capacity;
if (capacity == 0) {
emit WithdrawalForNonFlowRatedToken(token, amount);
return true;
}

// Calculate the depth assuming no withdrawal.
// slither-disable-next-line timestamp
uint256 depth = bucket.depth + (block.timestamp - bucket.refillTime) * bucket.refillRate;
// slither-disable-next-line timestamp
bucket.refillTime = block.timestamp;
// slither-disable-next-line timestamp
if (depth > capacity) {
depth = capacity;
}

// slither-disable-next-line timestamp
if (amount >= depth) {
// The bucket is empty indicating the flow rate is high. Automatically
// enable the withdrawal queue.
emit AutoActivatedWithdrawalQueue();
withdrawalQueueActivated = true;
bucket.depth = 0;
} else {
bucket.depth = depth - amount;
}
return false;
}

// slither-disable-next-line unused-state,naming-convention
uint256[50] private __gapFlowRateDetecton;
}
Loading

0 comments on commit a5a9f06

Please sign in to comment.