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

MultiAggregateRateLimiter - validation based contract with OffRamp implementation #916

Merged
merged 26 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8526467
feat: replace ARL with message validation hook on multi-offramp
elatoskinas May 21, 2024
5499569
feat: create multi-arl contract
elatoskinas May 21, 2024
f3f9899
feat: convert multi-arl contract to message validator
elatoskinas May 22, 2024
9843abe
perf: remove PriceRegistry from MultiOffRamp
elatoskinas May 22, 2024
f53a3bc
refactor: move multi-ARL to validators
elatoskinas May 22, 2024
18b7125
perf: remove PriceRegistry param from getTokenValue
elatoskinas May 22, 2024
ea6d0cd
feat: catch all validation failures and create IMessageValidator helper
elatoskinas May 22, 2024
6e15136
test: message validation in offramp
elatoskinas May 22, 2024
54d86d1
test: multi-arl tests
elatoskinas May 22, 2024
0841537
chore: create wrappers, snapshot & changesets
elatoskinas May 23, 2024
b67f9c2
refactor: move multi-arl back to root contracts dir
elatoskinas May 23, 2024
b56c0e8
perf: remove unnecessary variable caching and style changes
elatoskinas May 24, 2024
74717ed
style: rename chainSelector to remoteChainSelector
elatoskinas May 27, 2024
c0e4414
feat: remove admin from multi-arl
elatoskinas May 27, 2024
d09db46
refactor: remove duplicated RateLimiter lib
elatoskinas May 27, 2024
4183828
refactor: convert authorized callers to set with getter
elatoskinas May 27, 2024
184ed13
refactor: rename tokens to local / remote
elatoskinas May 27, 2024
6add009
feat: separate rate limits for incoming and outgoing lanes
elatoskinas May 27, 2024
ba202a7
refactor: rename validator to IMessageInterceptor
elatoskinas May 28, 2024
27efcd7
refactor: replace bit manipulation approach with structs for lane sta…
elatoskinas May 28, 2024
f3b7b81
chore: generate wrappers
elatoskinas May 28, 2024
9da9e68
Merge branch 'ccip-develop' into feat/multi-offramp-arl-hook
elatoskinas May 29, 2024
4bb71ef
chore: re-gen snapshots and wrappers
elatoskinas May 29, 2024
49d28b4
fix: non-ccip receiver message validation for offramp
elatoskinas May 29, 2024
f2a5195
Merge branch 'ccip-develop' into feat/multi-offramp-arl-hook
RensR May 30, 2024
0faa19f
Merge branch 'ccip-develop' into feat/multi-offramp-arl-hook
elatoskinas Jun 3, 2024
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
5 changes: 5 additions & 0 deletions .changeset/few-spies-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ccip": minor
---

#changed Implement hook-based multi-aggregate rate limiter
5 changes: 5 additions & 0 deletions contracts/.changeset/modern-bees-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chainlink/contracts-ccip": minor
---

#changed Implement hook-based multi-aggregate rate limiter
281 changes: 167 additions & 114 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

93 changes: 0 additions & 93 deletions contracts/src/v0.8/ccip/MultiAggregateRateLimiter.sol

This file was deleted.

22 changes: 22 additions & 0 deletions contracts/src/v0.8/ccip/interfaces/IMessageValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Client} from "../libraries/Client.sol";

/// @notice Interface for plug-in message validator contracts that intercept OffRamp & OnRamp messages
/// and perform validations on top of the messages. All validation functions are expected to
/// revert on validation failures.
interface IMessageValidator {
/// @notice Common error that can be thrown on validation failures and used by consumers
/// @param errorReason abi encoded revert reason
error MessageValidationError(bytes errorReason);

/// @notice Validates the given OffRamp message. Reverts on validation failure
/// @param message to validate
function validateIncomingMessage(Client.Any2EVMMessage memory message) external;
elatoskinas marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Validates the given OnRamp message. Reverts on validation failure
/// @param message to valdidate
/// @param destChainSelector dest chain selector where the message is being sent to
function validateOutgoingMessage(Client.EVM2AnyMessage memory message, uint64 destChainSelector) external;
}
152 changes: 152 additions & 0 deletions contracts/src/v0.8/ccip/libraries/RateLimiterNoEvents.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/// @notice Implements Token Bucket rate limiting.
/// @dev uint128 is safe for rate limiter state.
/// For USD value rate limiting, it can adequately store USD value in 18 decimals.
/// For ERC20 token amount rate limiting, all tokens that will be listed will have at most
/// a supply of uint128.max tokens, and it will therefore not overflow the bucket.
/// In exceptional scenarios where tokens consumed may be larger than uint128,
/// e.g. compromised issuer, an enabled RateLimiter will check and revert.
/// This version of the RateLimiter delegates event emissions to the consumer.
library RateLimiterNoEvents {
elatoskinas marked this conversation as resolved.
Show resolved Hide resolved
error BucketOverfilled();
error OnlyCallableByAdminOrOwner();
error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress);
error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress);
error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested);
error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available);
error InvalidRatelimitRate(Config rateLimiterConfig);
error DisabledNonZeroRateLimit(Config config);
error RateLimitMustBeDisabled();

struct TokenBucket {
uint128 tokens; // ──────╮ Current number of tokens that are in the bucket.
uint32 lastUpdated; // │ Timestamp in seconds of the last token refill, good for 100+ years.
bool isEnabled; // ──────╯ Indication whether the rate limiting is enabled or not
uint128 capacity; // ────╮ Maximum number of tokens that can be in the bucket.
uint128 rate; // ────────╯ Number of tokens per second that the bucket is refilled.
}

struct Config {
bool isEnabled; // Indication whether the rate limiting should be enabled
uint128 capacity; // ────╮ Specifies the capacity of the rate limiter
uint128 rate; // ───────╯ Specifies the rate of the rate limiter
}

/// @notice _consume removes the given tokens from the pool, lowering the
/// rate tokens allowed to be consumed for subsequent calls.
/// @param requestTokens The total tokens to be consumed from the bucket.
/// @param tokenAddress The token to consume capacity for, use 0x0 to indicate aggregate value capacity.
/// @dev Reverts when requestTokens exceeds bucket capacity or available tokens in the bucket
/// @dev emits removal of requestTokens if requestTokens is > 0
function _consume(TokenBucket storage s_bucket, uint256 requestTokens, address tokenAddress) internal {
// If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage
if (!s_bucket.isEnabled || requestTokens == 0) {
return;
}

uint256 tokens = s_bucket.tokens;
uint256 capacity = s_bucket.capacity;
uint256 timeDiff = block.timestamp - s_bucket.lastUpdated;

if (timeDiff != 0) {
if (tokens > capacity) revert BucketOverfilled();

// Refill tokens when arriving at a new block time
tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate);

s_bucket.lastUpdated = uint32(block.timestamp);
}

if (capacity < requestTokens) {
// Token address 0 indicates consuming aggregate value rate limit capacity.
if (tokenAddress == address(0)) revert AggregateValueMaxCapacityExceeded(capacity, requestTokens);
revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress);
}
if (tokens < requestTokens) {
uint256 rate = s_bucket.rate;
// Wait required until the bucket is refilled enough to accept this value, round up to next higher second
// Consume is not guaranteed to succeed after wait time passes if there is competing traffic.
// This acts as a lower bound of wait time.
uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate;

if (tokenAddress == address(0)) revert AggregateValueRateLimitReached(minWaitInSeconds, tokens);
revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress);
}
tokens -= requestTokens;

// Downcast is safe here, as tokens is not larger than capacity
s_bucket.tokens = uint128(tokens);
}

/// @notice Gets the token bucket with its values for the block it was requested at.
/// @return The token bucket.
function _currentTokenBucketState(TokenBucket memory bucket) internal view returns (TokenBucket memory) {
// We update the bucket to reflect the status at the exact time of the
// call. This means we might need to refill a part of the bucket based
// on the time that has passed since the last update.
bucket.tokens =
uint128(_calculateRefill(bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate));
bucket.lastUpdated = uint32(block.timestamp);
return bucket;
}

/// @notice Sets the rate limited config.
/// @param s_bucket The token bucket
/// @param config The new config
function _setTokenBucketConfig(TokenBucket storage s_bucket, Config memory config) internal {
// First update the bucket to make sure the proper rate is used for all the time
// up until the config change.
uint256 timeDiff = block.timestamp - s_bucket.lastUpdated;
if (timeDiff != 0) {
s_bucket.tokens = uint128(_calculateRefill(s_bucket.capacity, s_bucket.tokens, timeDiff, s_bucket.rate));

s_bucket.lastUpdated = uint32(block.timestamp);
}

s_bucket.tokens = uint128(_min(config.capacity, s_bucket.tokens));
s_bucket.isEnabled = config.isEnabled;
s_bucket.capacity = config.capacity;
s_bucket.rate = config.rate;
}

/// @notice Validates the token bucket config
function _validateTokenBucketConfig(Config memory config, bool mustBeDisabled) internal pure {
if (config.isEnabled) {
if (config.rate >= config.capacity || config.rate == 0) {
revert InvalidRatelimitRate(config);
}
if (mustBeDisabled) {
revert RateLimitMustBeDisabled();
}
} else {
if (config.rate != 0 || config.capacity != 0) {
revert DisabledNonZeroRateLimit(config);
}
}
}

/// @notice Calculate refilled tokens
/// @param capacity bucket capacity
/// @param tokens current bucket tokens
/// @param timeDiff block time difference since last refill
/// @param rate bucket refill rate
/// @return the value of tokens after refill
function _calculateRefill(
uint256 capacity,
uint256 tokens,
uint256 timeDiff,
uint256 rate
) private pure returns (uint256) {
return _min(capacity, tokens + timeDiff * rate);
}

/// @notice Return the smallest of two integers
/// @param a first int
/// @param b second int
/// @return smallest
function _min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}
Loading
Loading