Skip to content

Commit

Permalink
Family-agnostic ramps - refactor fee and family-specific logic to Pri…
Browse files Browse the repository at this point in the history
…ceRegistry (#1181)

## Motivation
Lifts out family-specific fee & validation logic to the PriceRegistry -
isolating fees calculations to the PriceRegistry state. The refactor
converts the OnRamp to be fully dest-chain family agnostic - future
additions of chain families would typically require only changing the
PriceRegistry.

## Solution
* Move the `DestChainConfig` to the PriceRegistry, and completely remove
dest chain configs from the OnRamp. Since most of the dynamic config is
specific to fee & chain logic (with the exception of `maxDataBytes` /
`maxNumberOfTokens`), it is optimal to store the configs in a single
contract
* Moves all fee token configs to the PriceRegistry
* Removes duplicate `_validateMessage`. Since the Router always calls
`getFee` before `forwardFromRouter` - we can do the validations early in
`getFee`
* `PriceRegistry.getValidatedFee` - performs the previous `getFee` logic
* `PriceRegistry.getValidatedRampMessageParams` - performs the
chain-specific logic of `forwardFromRouter` - validating pool return
data, computing message fees and determining out-of-order exec

![alt-chain-family-handling-design-Page-2 drawio
(1)](https://github.com/user-attachments/assets/79603dde-6531-4d17-bf8b-ae80eaf0dbfc)


### Contract Sizes

| Contract | Prev Size (kB) | Prev Margin (kB) | New Size (kB) | New
Margin (kB) |

|---------------------------------|------------------------------|---------------------------|---------------------------|---------------------------|
| EVM2EVMMultiOnRamp | 22.6 | 1.976 | 9.944 | 14.632 |
| PriceRegistry | 7.46 | 17.116 | 18.439 | 6.137 |
| **Total** | 30.06 | | 28.383 | |

### Gas costs (e2e)

| Function | Prev min-max | Prev avg | New min-max | New avg |

|--------------|--------------|---------------|----------------|---------|
| getFee (MultiOnRamp) | 0 - 47 501 | 20 357 | 0 - 38 863 | 16 655
| ccipSend (Router) | 287 347 - 375 060 | 341 643 | 279 728 - 367 441 |
334 024

---------

Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
Co-authored-by: Makram <[email protected]>
  • Loading branch information
3 people authored Jul 19, 2024
1 parent be120a2 commit ef923c3
Show file tree
Hide file tree
Showing 26 changed files with 5,173 additions and 3,994 deletions.
695 changes: 353 additions & 342 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/scripts/native_solc_compile_all_ccip
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ compileContract () {
echo "MultiOffRamp uses $OPTIMIZE_RUNS_MULTI_OFFRAMP optimizer runs."
optimize_runs=$OPTIMIZE_RUNS_MULTI_OFFRAMP
;;
"ccip/onRamp/EVM2EVMMultiOnRamp.sol" | "ccip/onRamp/EVM2EVMOnRamp.sol")
"ccip/onRamp/EVM2EVMOnRamp.sol")
echo "OnRamp uses $OPTIMIZE_RUNS_ONRAMP optimizer runs."
optimize_runs=$OPTIMIZE_RUNS_ONRAMP
;;
Expand Down
605 changes: 584 additions & 21 deletions contracts/src/v0.8/ccip/PriceRegistry.sol

Large diffs are not rendered by default.

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

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

interface IPriceRegistry {
Expand Down Expand Up @@ -71,4 +72,38 @@ interface IPriceRegistry {
/// @notice Get the list of fee tokens.
/// @return The tokens set as fee tokens.
function getFeeTokens() external view returns (address[] memory);

/// @notice Validates the ccip message & returns the fee
/// @param destChainSelector The destination chain selector.
/// @param message The message to get quote for.
/// @return feeTokenAmount The amount of fee token needed for the fee, in smallest denomination of the fee token.
function getValidatedFee(
uint64 destChainSelector,
Client.EVM2AnyMessage calldata message
) external view returns (uint256 feeTokenAmount);

/// @notice Converts the extraArgs to the latest version and returns the converted message fee in juels
/// @param destChainSelector destination chain selector to process
/// @param feeToken Fee token address used to pay for message fees
/// @param feeTokenAmount Fee token amount
/// @param extraArgs Message extra args that were passed in by the client
/// @return msgFeeJuels message fee in juels
/// @return isOutOfOrderExecution true if the message should be executed out of order
/// @return convertedExtraArgs extra args converted to the latest family-specific args version
function processMessageArgs(
uint64 destChainSelector,
address feeToken,
uint256 feeTokenAmount,
bytes memory extraArgs
) external view returns (uint256 msgFeeJuels, bool isOutOfOrderExecution, bytes memory convertedExtraArgs);

/// @notice Validates pool return data
/// @param destChainSelector Destination chain selector to which the token amounts are sent to
/// @param rampTokenAmounts Token amounts with populated pool return data
/// @param sourceTokenAmounts Token amounts originally sent in a Client.EVM2AnyMessage message
function validatePoolReturnData(
uint64 destChainSelector,
Internal.RampTokenAmount[] calldata rampTokenAmounts,
Client.EVMTokenAmount[] calldata sourceTokenAmounts
) external view;
}
731 changes: 89 additions & 642 deletions contracts/src/v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/src/v0.8/ccip/test/BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract BaseTest is Test {
// Message info
uint64 internal constant SOURCE_CHAIN_SELECTOR = 1;
uint64 internal constant DEST_CHAIN_SELECTOR = 2;
uint64 internal constant GAS_LIMIT = 200_000;
uint32 internal constant GAS_LIMIT = 200_000;

// Timing
uint256 internal constant BLOCK_TIME = 1234567890;
Expand Down
2 changes: 0 additions & 2 deletions contracts/src/v0.8/ccip/test/NonceManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,6 @@ contract NonceManager_OnRampUpgrade is EVM2EVMMultiOnRampSetup {
NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(address(s_prevOnRamp), address(0)));
s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps);

EVM2EVMMultiOnRamp.DestChainConfigArgs[] memory destChainConfigArgs = _generateDestChainConfigArgs();

(s_onRamp, s_metadataHash) = _deployOnRamp(
SOURCE_CHAIN_SELECTOR, address(s_sourceRouter), address(s_outboundNonceManager), address(s_tokenAdminRegistry)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {Client} from "../../../libraries/Client.sol";
import {Internal} from "../../../libraries/Internal.sol";
import {EVM2EVMMultiOnRamp} from "../../../onRamp/EVM2EVMMultiOnRamp.sol";
import {TokenPool} from "../../../pools/TokenPool.sol";
import {EVM2EVMMultiOnRampSetup} from "../../onRamp/EVM2EVMMultiOnRampSetup.t.sol";
import {FacadeClient} from "./FacadeClient.sol";
import {ReentrantMaliciousTokenPool} from "./ReentrantMaliciousTokenPool.sol";

import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";

import {console} from "forge-std/console.sol";

/// @title MultiOnRampTokenPoolReentrancy
/// Attempts to perform a reentrancy exploit on Onramp with a malicious TokenPool
contract MultiOnRampTokenPoolReentrancy is EVM2EVMMultiOnRampSetup {
FacadeClient internal s_facadeClient;
ReentrantMaliciousTokenPool internal s_maliciousTokenPool;
IERC20 internal s_sourceToken;
IERC20 internal s_feeToken;
address internal immutable i_receiver = makeAddr("receiver");

function setUp() public virtual override {
EVM2EVMMultiOnRampSetup.setUp();

s_sourceToken = IERC20(s_sourceTokens[0]);
s_feeToken = IERC20(s_sourceTokens[0]);

s_facadeClient =
new FacadeClient(address(s_sourceRouter), DEST_CHAIN_SELECTOR, s_sourceToken, s_feeToken, i_receiver);

s_maliciousTokenPool = new ReentrantMaliciousTokenPool(
address(s_facadeClient), s_sourceToken, address(s_mockRMN), address(s_sourceRouter)
);

TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1);
chainUpdates[0] = TokenPool.ChainUpdate({
remoteChainSelector: DEST_CHAIN_SELECTOR,
remotePoolAddress: abi.encode(s_destPoolBySourceToken[s_sourceTokens[0]]),
remoteTokenAddress: abi.encode(s_destTokens[0]),
allowed: true,
outboundRateLimiterConfig: getOutboundRateLimiterConfig(),
inboundRateLimiterConfig: getInboundRateLimiterConfig()
});
s_maliciousTokenPool.applyChainUpdates(chainUpdates);
s_sourcePoolByToken[address(s_sourceToken)] = address(s_maliciousTokenPool);

Internal.PoolUpdate[] memory removes = new Internal.PoolUpdate[](1);
removes[0].token = address(s_sourceToken);
removes[0].pool = address(s_sourcePoolByToken[address(s_sourceToken)]);
Internal.PoolUpdate[] memory adds = new Internal.PoolUpdate[](1);
adds[0].token = address(s_sourceToken);
adds[0].pool = address(s_maliciousTokenPool);

s_tokenAdminRegistry.setPool(address(s_sourceToken), address(s_maliciousTokenPool));

s_sourceToken.transfer(address(s_facadeClient), 1e18);
s_feeToken.transfer(address(s_facadeClient), 1e18);
}

/// @dev This test was used to showcase a reentrancy exploit on OnRamp with malicious TokenPool.
/// How it worked: OnRamp used to construct EVM2Any messages after calling TokenPool's lockOrBurn.
/// This allowed the malicious TokenPool to break message sequencing expectations as follows:
/// Any user -> Facade -> 1st call to ccipSend -> pool’s lockOrBurn —>
/// (reenter)-> Facade -> 2nd call to ccipSend
/// In this case, Facade's second call would produce an EVM2Any msg with a lower sequence number.
/// The issue was fixed by moving state updates and event construction to before TokenPool calls.
/// This test is kept to verify message sequence expectations are not broken.
function test_OnRampTokenPoolReentrancy_Success() public {
uint256 amount = 1;

Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
tokenAmounts[0].token = address(s_sourceToken);
tokenAmounts[0].amount = amount;

Client.EVM2AnyMessage memory message1 = Client.EVM2AnyMessage({
receiver: abi.encode(i_receiver),
data: abi.encodePacked(uint256(1)), // message 1 contains data 1
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})),
feeToken: address(s_feeToken)
});

Client.EVM2AnyMessage memory message2 = Client.EVM2AnyMessage({
receiver: abi.encode(i_receiver),
data: abi.encodePacked(uint256(2)), // message 2 contains data 2
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})),
feeToken: address(s_feeToken)
});

uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message1);
assertGt(expectedFee, 0);

// Outcome of a successful exploit:
// Message 1 event from OnRamp contains sequence/nonce 2, message 2 contains sequence/nonce 1
// Internal.EVM2EVMMessage memory msgEvent1 = _messageToEvent(message1, 2, 2, expectedFee, address(s_facadeClient));
// Internal.EVM2EVMMessage memory msgEvent2 = _messageToEvent(message2, 1, 1, expectedFee, address(s_facadeClient));

// vm.expectEmit();
// emit CCIPSendRequested(msgEvent2);
// vm.expectEmit();
// emit CCIPSendRequested(msgEvent1);

// After issue is fixed, sequence now increments as expected
Internal.EVM2AnyRampMessage memory msgEvent1 = _messageToEvent(message1, 1, 1, expectedFee, address(s_facadeClient));
Internal.EVM2AnyRampMessage memory msgEvent2 = _messageToEvent(message2, 2, 2, expectedFee, address(s_facadeClient));

vm.expectEmit();
emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, msgEvent2);
vm.expectEmit();
emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, msgEvent1);

s_facadeClient.send(amount);
}
}
2 changes: 1 addition & 1 deletion contracts/src/v0.8/ccip/test/e2e/MultiRampsEnd2End.sol
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ contract MultiRampsE2E is EVM2EVMMultiOnRampSetup, EVM2EVMMultiOffRampSetup {
router.ccipSend(DEST_CHAIN_SELECTOR, message);
vm.pauseGasMetering();

uint256 gasLimit = abi.decode(msgEvent.extraArgs, (Client.EVMExtraArgsV2)).gasLimit;
uint256 gasLimit = s_priceRegistry.parseEVMExtraArgsFromBytes(msgEvent.extraArgs, DEST_CHAIN_SELECTOR).gasLimit;

return Internal.Any2EVMRampMessage({
header: Internal.RampMessageHeader({
Expand Down
54 changes: 2 additions & 52 deletions contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOnRampHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,6 @@ import {IgnoreContractSize} from "./IgnoreContractSize.sol";
contract EVM2EVMMultiOnRampHelper is EVM2EVMMultiOnRamp, IgnoreContractSize {
constructor(
StaticConfig memory staticConfig,
DynamicConfig memory dynamicConfig,
DestChainConfigArgs[] memory destChainConfigs,
PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs,
TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs
)
EVM2EVMMultiOnRamp(
staticConfig,
dynamicConfig,
destChainConfigs,
premiumMultiplierWeiPerEthArgs,
tokenTransferFeeConfigArgs
)
{}

function getDataAvailabilityCost(
uint64 destChainSelector,
uint112 dataAvailabilityGasPrice,
uint256 messageDataLength,
uint256 numberOfTokens,
uint32 tokenTransferBytesOverhead
) external view returns (uint256) {
return _getDataAvailabilityCost(
destChainSelector, dataAvailabilityGasPrice, messageDataLength, numberOfTokens, tokenTransferBytesOverhead
);
}

function getTokenTransferCost(
uint64 destChainSelector,
address feeToken,
uint224 feeTokenPrice,
Client.EVMTokenAmount[] calldata tokenAmounts
) external view returns (uint256, uint32, uint32) {
return _getTokenTransferCost(destChainSelector, feeToken, feeTokenPrice, tokenAmounts);
}

function parseEVMExtraArgsFromBytes(
bytes calldata extraArgs,
uint64 destChainSelector
) external view returns (Client.EVMExtraArgsV2 memory) {
return _parseEVMExtraArgsFromBytes(extraArgs, s_destChainConfig[destChainSelector].dynamicConfig);
}

function validateDestFamilyAddress(bytes4 chainFamilySelector, bytes memory destAddress) external pure {
_validateDestFamilyAddress(chainFamilySelector, destAddress);
}

function convertParsedExtraArgs(
bytes calldata extraArgs,
DestChainDynamicConfig memory destChainDynamicConfig
) external pure returns (bytes memory encodedExtraArgs) {
return _convertParsedExtraArgs(extraArgs, destChainDynamicConfig);
}
DynamicConfig memory dynamicConfig
) EVM2EVMMultiOnRamp(staticConfig, dynamicConfig) {}
}
72 changes: 72 additions & 0 deletions contracts/src/v0.8/ccip/test/helpers/PriceRegistryHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

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

contract PriceRegistryHelper is PriceRegistry {
constructor(
StaticConfig memory staticConfig,
address[] memory priceUpdaters,
address[] memory feeTokens,
TokenPriceFeedUpdate[] memory tokenPriceFeeds,
TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs,
PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs,
DestChainConfigArgs[] memory destChainConfigArgs
)
PriceRegistry(
staticConfig,
priceUpdaters,
feeTokens,
tokenPriceFeeds,
tokenTransferFeeConfigArgs,
premiumMultiplierWeiPerEthArgs,
destChainConfigArgs
)
{}

function getDataAvailabilityCost(
uint64 destChainSelector,
uint112 dataAvailabilityGasPrice,
uint256 messageDataLength,
uint256 numberOfTokens,
uint32 tokenTransferBytesOverhead
) external view returns (uint256) {
return _getDataAvailabilityCost(
s_destChainConfigs[destChainSelector],
dataAvailabilityGasPrice,
messageDataLength,
numberOfTokens,
tokenTransferBytesOverhead
);
}

function getTokenTransferCost(
uint64 destChainSelector,
address feeToken,
uint224 feeTokenPrice,
Client.EVMTokenAmount[] calldata tokenAmounts
) external view returns (uint256, uint32, uint32) {
return _getTokenTransferCost(
s_destChainConfigs[destChainSelector], destChainSelector, feeToken, feeTokenPrice, tokenAmounts
);
}

function parseEVMExtraArgsFromBytes(
bytes calldata extraArgs,
uint64 destChainSelector
) external view returns (Client.EVMExtraArgsV2 memory) {
return _parseEVMExtraArgsFromBytes(extraArgs, s_destChainConfigs[destChainSelector]);
}

function parseEVMExtraArgsFromBytes(
bytes calldata extraArgs,
DestChainConfig memory destChainConfig
) external pure returns (Client.EVMExtraArgsV2 memory) {
return _parseEVMExtraArgsFromBytes(extraArgs, destChainConfig);
}

function validateDestFamilyAddress(bytes4 chainFamilySelector, bytes memory destAddress) external pure {
_validateDestFamilyAddress(chainFamilySelector, destAddress);
}
}
2 changes: 1 addition & 1 deletion contracts/src/v0.8/ccip/test/mocks/MockRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ contract MockCCIPRouter is IRouter, IRouterClient {
event MsgExecuted(bool success, bytes retData, uint256 gasUsed);

uint16 public constant GAS_FOR_CALL_EXACT_CHECK = 5_000;
uint64 public constant DEFAULT_GAS_LIMIT = 200_000;
uint32 public constant DEFAULT_GAS_LIMIT = 200_000;

uint256 internal s_mockFeeTokenAmount; //use setFee() to change to non-zero to test fees

Expand Down
Loading

0 comments on commit ef923c3

Please sign in to comment.