Skip to content

Commit

Permalink
NonceManager outbound nonces (#985)
Browse files Browse the repository at this point in the history
## Motivation

Implements the outbound nonce logic for the `NonceManager` contract. The
goal of the `NonceManager` contract is to manage senders' nonces and
extract that logic from the ramps.

## Solution

- Onramp and offramp are listed as authorized callers by the owner
- Contract owner sets previous onramp addresses
- Previous ramps can only be set once
- `EVM2EVMMultiOnRamp` calls `incrementOutboundNonce` which returns the
new nonce
- Handles upgradability logic to call `prevOnRamp` if necessary

A shared `AuthorizedCallers` contract has been added and integrated to
the following contracts:
- `MultiAggregateRateLimiter`
- `NonceManager`

Out ouf scope:
- Inbound nonce logic (next PR)

---------

Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
  • Loading branch information
1 parent acb2d60 commit 20169bd
Show file tree
Hide file tree
Showing 22 changed files with 1,949 additions and 627 deletions.
110 changes: 55 additions & 55 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions contracts/gas-snapshots/shared.gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
AuthorizedCallers_applyAuthorizedCallerUpdates:test_AddAndRemove_Success() (gas: 125205)
AuthorizedCallers_applyAuthorizedCallerUpdates:test_OnlyAdd_Success() (gas: 133100)
AuthorizedCallers_applyAuthorizedCallerUpdates:test_OnlyCallableByOwner_Revert() (gas: 12350)
AuthorizedCallers_applyAuthorizedCallerUpdates:test_OnlyRemove_Success() (gas: 45064)
AuthorizedCallers_applyAuthorizedCallerUpdates:test_RemoveThenAdd_Success() (gas: 57241)
AuthorizedCallers_applyAuthorizedCallerUpdates:test_SkipRemove_Success() (gas: 32121)
AuthorizedCallers_applyAuthorizedCallerUpdates:test_ZeroAddressNotAllowed_Revert() (gas: 64473)
AuthorizedCallers_constructor:test_ZeroAddressNotAllowed_Revert() (gas: 64473)
AuthorizedCallers_constructor:test_constructor_Success() (gas: 720513)
BurnMintERC677_approve:testApproveSuccess() (gas: 55512)
BurnMintERC677_approve:testInvalidAddressReverts() (gas: 10663)
BurnMintERC677_burn:testBasicBurnSuccess() (gas: 173939)
Expand Down
116 changes: 28 additions & 88 deletions contracts/src/v0.8/ccip/MultiAggregateRateLimiter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity 0.8.24;
import {IMessageInterceptor} from "./interfaces/IMessageInterceptor.sol";
import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol";

import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol";
import {AuthorizedCallers} from "../shared/access/AuthorizedCallers.sol";
import {EnumerableMapAddresses} from "./../shared/enumerable/EnumerableMapAddresses.sol";
import {Client} from "./libraries/Client.sol";
import {RateLimiter} from "./libraries/RateLimiter.sol";
Expand All @@ -17,23 +17,19 @@ import {EnumerableSet} from "./../vendor/openzeppelin-solidity/v4.7.3/contracts/
/// token transfers, using a price registry to convert to a numeraire asset (e.g. USD).
/// The contract is a standalone multi-lane message validator contract, which can be called by authorized
/// ramp contracts to apply rate limit changes to lanes, and revert when the rate limits get breached.
contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {
contract MultiAggregateRateLimiter is IMessageInterceptor, AuthorizedCallers {
using RateLimiter for RateLimiter.TokenBucket;
using USDPriceWith18Decimals for uint224;
using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map;
using EnumerableSet for EnumerableSet.AddressSet;

error UnauthorizedCaller(address caller);
error PriceNotFoundForToken(address token);
error ZeroAddressNotAllowed();
error ZeroChainSelectorNotAllowed();

event RateLimiterConfigUpdated(uint64 indexed remoteChainSelector, bool isOutgoingLane, RateLimiter.Config config);
event RateLimiterConfigUpdated(uint64 indexed remoteChainSelector, bool isOutboundLane, RateLimiter.Config config);
event PriceRegistrySet(address newPriceRegistry);
event TokenAggregateRateLimitAdded(uint64 remoteChainSelector, bytes32 remoteToken, address localToken);
event TokenAggregateRateLimitRemoved(uint64 remoteChainSelector, address localToken);
event AuthorizedCallerAdded(address caller);
event AuthorizedCallerRemoved(address caller);

/// @notice RemoteRateLimitToken struct containing the local token address with the chain selector
/// The struct is used for removals and updates, since the local -> remote token mappings are scoped per-chain
Expand All @@ -48,54 +44,38 @@ contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {
bytes32 remoteToken; // Token on the remote chain (for OnRamp - dest, of OffRamp - source)
}

/// @notice Update args for changing the authorized callers
struct AuthorizedCallerArgs {
address[] addedCallers;
address[] removedCallers;
}

/// @notice Update args for a single rate limiter config update
struct RateLimiterConfigArgs {
uint64 remoteChainSelector; // ────╮ Chain selector to set config for
bool isOutgoingLane; // ───────────╯ If set to true, represents the outgoing message lane (OnRamp), and the incoming message lane otherwise (OffRamp)
bool isOutboundLane; // ───────────╯ If set to true, represents the outbound message lane (OnRamp), and the inbound message lane otherwise (OffRamp)
RateLimiter.Config rateLimiterConfig; // Rate limiter config to set
}

/// @notice Struct to store rate limit token buckets for both lane directions
struct RateLimiterBuckets {
RateLimiter.TokenBucket incomingLaneBucket; // Bucket for the incoming lane (remote -> local)
RateLimiter.TokenBucket outgoingLaneBucket; // Bucket for the outgoing lane (local -> remote)
RateLimiter.TokenBucket inboundLaneBucket; // Bucket for the inbound lane (remote -> local)
RateLimiter.TokenBucket outboundLaneBucket; // Bucket for the outbound lane (local -> remote)
}

/// @dev Tokens that should be included in Aggregate Rate Limiting (from local chain (this chain) -> remote),
/// grouped per-remote chain.
mapping(uint64 remoteChainSelector => EnumerableMapAddresses.AddressToBytes32Map tokensLocalToRemote) internal
s_rateLimitedTokensLocalToRemote;

/// @dev Set of callers that can call the validation functions (this is required since the validations modify state)
EnumerableSet.AddressSet internal s_authorizedCallers;

/// @notice The address of the PriceRegistry used to query token values for ratelimiting
address internal s_priceRegistry;

/// @notice Rate limiter token bucket states per chain, with separate buckets for incoming and outgoing lanes.
/// @notice Rate limiter token bucket states per chain, with separate buckets for inbound and outbound lanes.
mapping(uint64 remoteChainSelector => RateLimiterBuckets buckets) internal s_rateLimitersByChainSelector;

/// @param priceRegistry the price registry to set
/// @param authorizedCallers the authorized callers to set
constructor(address priceRegistry, address[] memory authorizedCallers) {
constructor(address priceRegistry, address[] memory authorizedCallers) AuthorizedCallers(authorizedCallers) {
_setPriceRegistry(priceRegistry);
_applyAuthorizedCallerUpdates(
AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)})
);
}

/// @inheritdoc IMessageInterceptor
function onIncomingMessage(Client.Any2EVMMessage memory message) external {
if (!s_authorizedCallers.contains(msg.sender)) {
revert UnauthorizedCaller(msg.sender);
}

function onInboundMessage(Client.Any2EVMMessage memory message) external onlyAuthorizedCallers {
uint64 remoteChainSelector = message.sourceChainSelector;
RateLimiter.TokenBucket storage tokenBucket = _getTokenBucket(remoteChainSelector, false);

Expand All @@ -114,23 +94,23 @@ contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {
}

/// @inheritdoc IMessageInterceptor
function onOutgoingMessage(Client.EVM2AnyMessage memory message, uint64 destChainSelector) external {
// TODO: to be implemented (assuming the same rate limiter states are shared for incoming and outgoing messages)
function onOutboundMessage(Client.EVM2AnyMessage memory message, uint64 destChainSelector) external {
// TODO: to be implemented (assuming the same rate limiter states are shared for inbound and outbound messages)
}

/// @param remoteChainSelector chain selector to retrieve token bucket for
/// @param isOutgoingLane if set to true, fetches the bucket for the outgoing message lane (OnRamp).
/// Otherwise fetches for the incoming message lane (OffRamp).
/// @param isOutboundLane if set to true, fetches the bucket for the outbound message lane (OnRamp).
/// Otherwise fetches for the inbound message lane (OffRamp).
/// @return bucket Storage pointer to the token bucket representing a specific lane
function _getTokenBucket(
uint64 remoteChainSelector,
bool isOutgoingLane
bool isOutboundLane
) internal view returns (RateLimiter.TokenBucket storage) {
RateLimiterBuckets storage rateLimiterBuckets = s_rateLimitersByChainSelector[remoteChainSelector];
if (isOutgoingLane) {
return rateLimiterBuckets.outgoingLaneBucket;
if (isOutboundLane) {
return rateLimiterBuckets.outboundLaneBucket;
} else {
return rateLimiterBuckets.incomingLaneBucket;
return rateLimiterBuckets.inboundLaneBucket;
}
}

Expand All @@ -146,15 +126,15 @@ contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {

/// @notice Gets the token bucket with its values for the block it was requested at.
/// @param remoteChainSelector chain selector to retrieve state for
/// @param isOutgoingLane if set to true, fetches the rate limit state for the outgoing message lane (OnRamp).
/// Otherwise fetches for the incoming message lane (OffRamp).
/// The outgoing and incoming message rate limit state is completely separated.
/// @param isOutboundLane if set to true, fetches the rate limit state for the outbound message lane (OnRamp).
/// Otherwise fetches for the inbound message lane (OffRamp).
/// The outbound and inbound message rate limit state is completely separated.
/// @return The token bucket.
function currentRateLimiterState(
uint64 remoteChainSelector,
bool isOutgoingLane
bool isOutboundLane
) external view returns (RateLimiter.TokenBucket memory) {
return _getTokenBucket(remoteChainSelector, isOutgoingLane)._currentTokenBucketState();
return _getTokenBucket(remoteChainSelector, isOutboundLane)._currentTokenBucketState();
}

/// @notice Applies the provided rate limiter config updates.
Expand All @@ -170,9 +150,9 @@ contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {
revert ZeroChainSelectorNotAllowed();
}

bool isOutgoingLane = updateArgs.isOutgoingLane;
bool isOutboundLane = updateArgs.isOutboundLane;

RateLimiter.TokenBucket storage tokenBucket = _getTokenBucket(remoteChainSelector, isOutgoingLane);
RateLimiter.TokenBucket storage tokenBucket = _getTokenBucket(remoteChainSelector, isOutboundLane);

if (tokenBucket.lastUpdated == 0) {
// Token bucket needs to be newly added
Expand All @@ -184,15 +164,15 @@ contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {
isEnabled: configUpdate.isEnabled
});

if (isOutgoingLane) {
s_rateLimitersByChainSelector[remoteChainSelector].outgoingLaneBucket = newTokenBucket;
if (isOutboundLane) {
s_rateLimitersByChainSelector[remoteChainSelector].outboundLaneBucket = newTokenBucket;
} else {
s_rateLimitersByChainSelector[remoteChainSelector].incomingLaneBucket = newTokenBucket;
s_rateLimitersByChainSelector[remoteChainSelector].inboundLaneBucket = newTokenBucket;
}
} else {
tokenBucket._setTokenBucketConfig(configUpdate);
}
emit RateLimiterConfigUpdated(remoteChainSelector, isOutgoingLane, configUpdate);
emit RateLimiterConfigUpdated(remoteChainSelector, isOutboundLane, configUpdate);
}
}

Expand Down Expand Up @@ -276,44 +256,4 @@ contract MultiAggregateRateLimiter is IMessageInterceptor, OwnerIsCreator {
s_priceRegistry = newPriceRegistry;
emit PriceRegistrySet(newPriceRegistry);
}

// ================================================================
// │ Access │
// ================================================================

/// @return authorizedCallers Returns all callers that are authorized to call the validation functions
function getAllAuthorizedCallers() external view returns (address[] memory) {
return s_authorizedCallers.values();
}

/// @notice Updates the callers that are authorized to call the message validation functions
/// @param authorizedCallerArgs Callers to add and remove
function applyAuthorizedCallerUpdates(AuthorizedCallerArgs memory authorizedCallerArgs) external onlyOwner {
_applyAuthorizedCallerUpdates(authorizedCallerArgs);
}

/// @notice Updates the callers that are authorized to call the message validation functions
/// @param authorizedCallerArgs Callers to add and remove
function _applyAuthorizedCallerUpdates(AuthorizedCallerArgs memory authorizedCallerArgs) internal {
address[] memory removedCallers = authorizedCallerArgs.removedCallers;
for (uint256 i = 0; i < removedCallers.length; ++i) {
address caller = removedCallers[i];

if (s_authorizedCallers.remove(caller)) {
emit AuthorizedCallerRemoved(caller);
}
}

address[] memory addedCallers = authorizedCallerArgs.addedCallers;
for (uint256 i = 0; i < addedCallers.length; ++i) {
address caller = addedCallers[i];

if (caller == address(0)) {
revert ZeroAddressNotAllowed();
}

s_authorizedCallers.add(caller);
emit AuthorizedCallerAdded(caller);
}
}
}
96 changes: 96 additions & 0 deletions contracts/src/v0.8/ccip/NonceManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {IEVM2AnyOnRamp} from "./interfaces/IEVM2AnyOnRamp.sol";
import {INonceManager} from "./interfaces/INonceManager.sol";

import {AuthorizedCallers} from "../shared/access/AuthorizedCallers.sol";

/// @title NonceManager
/// @notice NonceManager contract that manages sender nonces for the on/off ramps
contract NonceManager is INonceManager, AuthorizedCallers {
error PreviousRampAlreadySet();

event PreviousOnRampUpdated(uint64 indexed destChainSelector, address prevOnRamp);

/// @dev Struct that contains the previous on/off ramp addresses
// TODO: add prevOffRamp
struct PreviousRamps {
address prevOnRamp; // Previous onRamp
}

/// @dev Struct that contains the chain selector and the previous on/off ramps, same as PreviousRamps but with the chain selector
/// so that an array of these can be passed to the applyPreviousRampsUpdates function
struct PreviousRampsArgs {
uint64 remoteChainSelector; // Chain selector
PreviousRamps prevRamps; // Previous on/off ramps
}

/// @dev previous ramps
mapping(uint64 chainSelector => PreviousRamps previousRamps) private s_previousRamps;
/// @dev The current outbound nonce per sender used on the onramp
mapping(uint64 destChainSelector => mapping(address sender => uint64 outboundNonce)) private s_outboundNonces;

constructor(address[] memory authorizedCallers) AuthorizedCallers(authorizedCallers) {}

/// @inheritdoc INonceManager
function getIncrementedOutboundNonce(
uint64 destChainSelector,
address sender
) external onlyAuthorizedCallers returns (uint64) {
uint64 outboundNonce = _getOutboundNonce(destChainSelector, sender) + 1;
s_outboundNonces[destChainSelector][sender] = outboundNonce;

return outboundNonce;
}

/// TODO: add incrementInboundNonce

/// @notice Returns the outbound nonce for the given sender on the given destination chain
/// @param destChainSelector The destination chain selector
/// @param sender The sender address
/// @return The outbound nonce
function getOutboundNonce(uint64 destChainSelector, address sender) external view returns (uint64) {
return _getOutboundNonce(destChainSelector, sender);
}

function _getOutboundNonce(uint64 destChainSelector, address sender) private view returns (uint64) {
uint64 outboundNonce = s_outboundNonces[destChainSelector][sender];

if (outboundNonce == 0) {
address prevOnRamp = s_previousRamps[destChainSelector].prevOnRamp;
if (prevOnRamp != address(0)) {
return IEVM2AnyOnRamp(prevOnRamp).getSenderNonce(sender);
}
}

return outboundNonce;
}

/// TODO: add getInboundNonce

/// @notice Updates the previous ramps addresses
/// @param previousRampsArgs The previous on/off ramps addresses
function applyPreviousRampsUpdates(PreviousRampsArgs[] calldata previousRampsArgs) external onlyOwner {
for (uint256 i = 0; i < previousRampsArgs.length; ++i) {
PreviousRampsArgs calldata previousRampsArg = previousRampsArgs[i];

PreviousRamps storage prevRamps = s_previousRamps[previousRampsArg.remoteChainSelector];

// If the previous onRamp is already set then it should not be updated
if (prevRamps.prevOnRamp != address(0)) {
revert PreviousRampAlreadySet();
}

prevRamps.prevOnRamp = previousRampsArg.prevRamps.prevOnRamp;
emit PreviousOnRampUpdated(previousRampsArg.remoteChainSelector, prevRamps.prevOnRamp);
}
}

/// @notice Gets the previous onRamp address for the given chain selector
/// @param chainSelector The chain selector
/// @return The previous onRamp address
function getPreviousRamps(uint64 chainSelector) external view returns (PreviousRamps memory) {
return s_previousRamps[chainSelector];
}
}
17 changes: 0 additions & 17 deletions contracts/src/v0.8/ccip/interfaces/IEVM2AnyMultiOnRamp.sol

This file was deleted.

4 changes: 2 additions & 2 deletions contracts/src/v0.8/ccip/interfaces/IMessageInterceptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ interface IMessageInterceptor {

/// @notice Intercepts & validates the given OffRamp message. Reverts on validation failure
/// @param message to validate
function onIncomingMessage(Client.Any2EVMMessage memory message) external;
function onInboundMessage(Client.Any2EVMMessage memory message) external;

/// @notice Intercepts & validates the given OnRamp message. Reverts on validation failure
/// @param message to validate
/// @param destChainSelector remote destination chain selector where the message is being sent to
function onOutgoingMessage(Client.EVM2AnyMessage memory message, uint64 destChainSelector) external;
function onOutboundMessage(Client.EVM2AnyMessage memory message, uint64 destChainSelector) external;
}
11 changes: 11 additions & 0 deletions contracts/src/v0.8/ccip/interfaces/INonceManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @notice Contract interface that allows managing sender nonces
interface INonceManager {
/// @notice Increments the outbound nonce for the given sender on the given destination chain
/// @param destChainSelector The destination chain selector
/// @param sender The sender address
/// @return The new outbound nonce
function getIncrementedOutboundNonce(uint64 destChainSelector, address sender) external returns (uint64);
}
2 changes: 1 addition & 1 deletion contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ contract EVM2EVMMultiOffRamp is IAny2EVMMultiOffRamp, ITypeAndVersion, MultiOCR3

address messageValidator = s_dynamicConfig.messageValidator;
if (messageValidator != address(0)) {
try IMessageInterceptor(messageValidator).onIncomingMessage(any2EvmMessage) {}
try IMessageInterceptor(messageValidator).onInboundMessage(any2EvmMessage) {}
catch (bytes memory err) {
revert IMessageInterceptor.MessageValidationError(err);
}
Expand Down
Loading

0 comments on commit 20169bd

Please sign in to comment.