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

SMR-1677 Flow Rate #41

Merged
merged 58 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
9c9ecaa
WIP
proletesseract Nov 7, 2023
c5bef71
Merge branch 'main' into SMR-1677-FlowRate
proletesseract Nov 7, 2023
d0326eb
flow rate initial port complete
proletesseract Nov 8, 2023
0061c76
all code ported, some failing tests
proletesseract Nov 13, 2023
d2127d3
integration tests passing
proletesseract Nov 13, 2023
3e3d9ff
Merge branch 'main' into SMR-1677-FlowRate
proletesseract Nov 13, 2023
a952622
compiling, couple tests to patch
proletesseract Nov 13, 2023
43f8499
tests passing again
proletesseract Nov 13, 2023
1cc475d
Fix linting issue
ermyas Nov 13, 2023
d7d7bb3
setup unit tests for flowRate
proletesseract Nov 14, 2023
26e1764
WIP
proletesseract Nov 14, 2023
4b27639
Refactor withdrawal validation
ermyas Nov 14, 2023
51a246f
Replace old references to RootERC20Predicate
ermyas Nov 14, 2023
17f7170
test for getter/setters
proletesseract Nov 14, 2023
ef8f6d4
Merge branch 'SMR-1677-FlowRate' of https://github.com/immutable/zkev…
proletesseract Nov 14, 2023
a1ea466
test WIP
proletesseract Nov 14, 2023
589fbb7
more tests ported
proletesseract Nov 15, 2023
de763bb
Update RootERC20BridgeFlowRate.t.sol
proletesseract Nov 15, 2023
f8aac68
patch for native eth withdrawals
proletesseract Nov 15, 2023
aacca8b
Merge branch 'main' into SMR-1677-FlowRate
proletesseract Nov 16, 2023
f6cfff8
some tests fixed
proletesseract Nov 16, 2023
665998b
Update RootERC20BridgeFlowRate.t.sol
proletesseract Nov 16, 2023
a8ab7af
more tests
proletesseract Nov 16, 2023
d8c29d9
all tests ported and passing except 1
proletesseract Nov 16, 2023
597c36c
Merge branch 'main' into SMR-1677-FlowRate
proletesseract Nov 16, 2023
50719dc
merge fixes
proletesseract Nov 16, 2023
9e7da29
formatted
proletesseract Nov 16, 2023
a212c68
Update src/interfaces/root/IRootERC20Bridge.sol
proletesseract Nov 16, 2023
dbb404b
Update src/interfaces/root/IRootERC20Bridge.sol
proletesseract Nov 16, 2023
e7c7d5d
Merge branch 'main' into SMR-1677-FlowRate
proletesseract Nov 17, 2023
d4b8c7b
pause tests ported
proletesseract Nov 17, 2023
e516a76
formatting
proletesseract Nov 17, 2023
f9f8218
old rates emitted
proletesseract Nov 17, 2023
d19a6f6
revert if zero addresses for admins
proletesseract Nov 17, 2023
6b7aae1
emit previous delay
proletesseract Nov 17, 2023
89984eb
fixed duplicate emit of QueuedWithdrawl, split out interface
proletesseract Nov 17, 2023
7e2232e
Update src/root/flowrate/RootERC20BridgeFlowRate.sol
proletesseract Nov 17, 2023
2faf0b9
Update src/root/flowrate/RootERC20BridgeFlowRate.sol
proletesseract Nov 17, 2023
96c8a85
ermyas feedback complete
proletesseract Nov 17, 2023
bee0b43
Merge branch 'SMR-1677-FlowRate' of https://github.com/immutable/zkev…
proletesseract Nov 17, 2023
af3c550
more feedback
proletesseract Nov 17, 2023
20e54cb
removed unnecessary tests
proletesseract Nov 17, 2023
1234975
formatting
proletesseract Nov 17, 2023
2ecb076
error logs added
proletesseract Nov 17, 2023
96ec6e8
formatting
proletesseract Nov 17, 2023
924306a
Update src/root/flowrate/FlowRateDetection.sol
proletesseract Nov 19, 2023
9da89ea
Update test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol
proletesseract Nov 19, 2023
ff61a98
Update test/unit/root/flowrate/RootERC20BridgeFlowRate.t.sol
proletesseract Nov 19, 2023
919e0fc
Update src/interfaces/root/flowrate/IFlowRateWithdrawalQueue.sol
proletesseract Nov 19, 2023
6f75222
Update src/interfaces/root/flowrate/IFlowRateWithdrawalQueue.sol
proletesseract Nov 19, 2023
0b4422f
Update src/root/RootERC20Bridge.sol
proletesseract Nov 19, 2023
8448770
Update src/root/RootERC20Bridge.sol
proletesseract Nov 19, 2023
9316428
Update src/root/RootERC20Bridge.sol
proletesseract Nov 19, 2023
b90de74
pr feedback
proletesseract Nov 19, 2023
e774fec
Merge branch 'SMR-1677-FlowRate' of https://github.com/immutable/zkev…
proletesseract Nov 19, 2023
926bfaf
update comment
proletesseract Nov 19, 2023
8a94099
formatting
proletesseract Nov 19, 2023
0d2f16e
Update RootERC20Bridge.sol
proletesseract Nov 19, 2023
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
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
37 changes: 37 additions & 0 deletions src/interfaces/root/flowrate/IFlowRateWithdrawalQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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(
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
address indexed token, address indexed withdrawer, address indexed receiver, uint256 amount, uint256 index
);

// Indicates that the new withdrawal delay.
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
event WithdrawalDelayUpdated(uint256 delay, uint256 previousDelay);
}

interface IFlowRateWithdrawalQueueErrors {
// // A withdrawal was being processed, but the index is outside of the array.
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
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(
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
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(
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
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);
}
76 changes: 68 additions & 8 deletions src/root/RootERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ 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 {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
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";
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
import {
IRootERC20Bridge,
IERC20Metadata,
Expand All @@ -25,7 +29,14 @@ import {BridgeRoles} from "../common/BridgeRoles.sol";
* @dev Any checks or logic that is specific to the underlying messaging protocol should be done in the bridge adaptor.
* @dev Note that there is undefined behaviour for bridging non-standard ERC20 tokens (e.g. rebasing tokens). Please approach such cases with great care.
*/
contract RootERC20Bridge is IRootERC20Bridge, IRootERC20BridgeEvents, IRootERC20BridgeErrors, BridgeRoles {
contract RootERC20Bridge is
AccessControlUpgradeable, // AccessControlUpgradeable inherits Initializable
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
PausableUpgradeable,
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
IRootERC20Bridge,
IRootERC20BridgeEvents,
IRootERC20BridgeErrors,
BridgeRoles
{
using SafeERC20 for IERC20Metadata;

/// @dev leave this as the first param for the integration tests
Expand Down Expand Up @@ -71,7 +82,7 @@ contract RootERC20Bridge is IRootERC20Bridge, IRootERC20BridgeEvents, IRootERC20
* @param newRootWETHToken Address of ERC20 WETH on the root chain.
* @param newChildChain Name of child chain.
* @param newImxCumulativeDepositLimit The cumulative IMX deposit limit.
* @dev Can only be called once.
* @dev Can not actually be called directly, is overridden in RootERC20BridgeFlowRate
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
*/
function initialize(
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
InitializationRoles memory newRoles,
Expand All @@ -83,7 +94,44 @@ 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.
* @dev Can only be called once.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be called multiple times since its not gated by initializer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do we want to do about this? should we check if the roles or other variables set on the initialiser are set and if they are revert? so it can only be called once? Or is the fact its internal and only be accessed through the bridge or flowrate initializers enough?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It being internal is enough

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill just remove the dev comment then :)

*/
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 +445,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));
proletesseract marked this conversation as resolved.
Show resolved Hide resolved

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't rootIMXToken and NATIVE_ETH stored in the mapping, so you can check them all at once rather than doing an if/else?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I recall raising this a while back, in a different PR... the discussions around that are here for context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be split out to another ticket as to not hold up this PR

if (rootToken == rootIMXToken) {
childToken = NATIVE_IMX;
} else if (rootToken == NATIVE_ETH) {
Expand All @@ -411,7 +472,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 disabled for the token.
ermyas marked this conversation as resolved.
Show resolved Hide resolved
proletesseract marked this conversation as resolved.
Show resolved Hide resolved
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;
Comment on lines +116 to +121
Copy link
Collaborator

@ermyas ermyas Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirming my understanding here:

  1. we can't withdraw up to the full capacity of the bucket, it always has to be slightly lower. is this the expected behaviour? looking at testHighFlowRate() I get the impression that this might not the case?
  2. if a sizeable withdrawal (which is less than the large withdrawal threshold), triggers this condition, all other subsequent smaller withdrawals that otherwise could have been processed would also go into the queue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, i straight ported this file from the other repo. I haven't altered or reviewed this logic

} else {
bucket.depth = depth - amount;
}
return false;
}

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