stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: high
Invalid

`extraArgs` aren't properly encoded so all messages will revert

Vulnerability Details

According to Chainlink CCIP docs, when building and sending a message between chains, the message has to be formatted to the following struct:

Client.sol#L12-L18

struct EVM2AnyMessage {
bytes receiver;
bytes data;
struct Client.EVMTokenAmount[] tokenAmounts;
address feeToken;
bytes extraArgs;
}

and regarding extraArgs:

"Users fill in the EVMExtraArgsV1 struct then encode it to bytes using the _argsToBytes function"

Client.sol#L35-L37

function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) {
return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);
}

Currently this is how extraArgs are defined in the protocol:

RESDLTokenBridge.sol#L156-L164

function setExtraArgs(uint64 _chainSelector, bytes calldata _extraArgs) external onlyOwner {
❌ extraArgsByChain[_chainSelector] = _extraArgs;
emit SetExtraArgs(_chainSelector, _extraArgs);
}

Which won't work with CCIP because once the message is build using _buildCCIPMessage()

RESDLTokenBridge.sol#L194-L236

function _buildCCIPMessage(
address _receiver,
uint256 _tokenId,
ISDLPool.RESDLToken memory _reSDLToken,
address _destination,
address _feeTokenAddress,
bytes memory _extraArgs
) internal view returns (Client.EVM2AnyMessage memory) {
...
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
receiver: abi.encode(_destination),
data: abi.encode(
_receiver,
_tokenId,
_reSDLToken.amount,
_reSDLToken.boostAmount,
_reSDLToken.startTime,
_reSDLToken.duration,
_reSDLToken.expiry
),
tokenAmounts: tokenAmounts,
❌ extraArgs: _extraArgs,
feeToken: _feeTokenAddress
});
...
}

it's sent to Router::ccipSend() which calls onRamp::forwardFromRouter()

Router.sol#L145

function ccipSend(
uint64 destinationChainSelector,
Client.EVM2AnyMessage memory message
) external payable whenHealthy returns (bytes32) {
...
return IEVM2AnyOnRamp(onRamp).forwardFromRouter(message, feeTokenAmount, msg.sender); //@audit forward message to onRamp
}

which will try to decode extraArgs using _fromBytes()

File: @chainlink/contracts-ccip/src/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol
function forwardFromRouter(
Client.EVM2AnyMessage calldata message,
uint256 feeTokenAmount,
address originalSender
) external whenHealthy returns (bytes32) {
...
❌ Client.EVMExtraArgsV1 memory extraArgs = _fromBytes(message.extraArgs);
...
}

But the _fromBytes() function will revert if extraArgs.length isn't 0 and if (bytes4(extraArgs) != Client.EVM_EXTRA_ARGS_V1_TAG)

EVM2EVMOnRamp.sol#L360-L371

function _fromBytes(bytes calldata extraArgs) internal view returns (Client.EVMExtraArgsV1 memory) {
if (extraArgs.length == 0) {
return Client.EVMExtraArgsV1({gasLimit: i_defaultTxGasLimit, strict: false});
}
if (bytes4(extraArgs) != Client.EVM_EXTRA_ARGS_V1_TAG) revert InvalidExtraArgsTag(); //@audit it will always revert here
return abi.decode(extraArgs[4:], (Client.EVMExtraArgsV1));
}

The tests are passing because it's using a mock which doesn't have the _fromBytes() function

CCIPOnRampMock.sol#L49-L57

function forwardFromRouter(
Client.EVM2AnyMessage calldata _message,
uint256 _feeTokenAmount,
address _originalSender
) external returns (bytes32) {
requestMessages.push(_message);
requestData.push(RequestData(_feeTokenAmount, _originalSender));
return keccak256(abi.encode(block.timestamp));
}

Impact

All messages sent by:

  • contracts/core/ccip/RESDLTokenBridge.sol

  • contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol

  • contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol

  • contracts/core/ccip/WrappedTokenBridge.sol

using their _buildCCIPMessage() function will revert because extraArgs aren't properly encoded.

PoC

Add this to the CCIPOnRampMock.sol mock and run tests:

File: contracts/core/test/chainlink/CCIPOnRampMock.sol
function forwardFromRouter(
Client.EVM2AnyMessage calldata _message,
uint256 _feeTokenAmount,
address _originalSender
) external returns (bytes32) {
requestMessages.push(_message);
requestData.push(RequestData(_feeTokenAmount, _originalSender));
+ Client.EVMExtraArgsV1 memory extraArgs = _fromBytes(_message.extraArgs);
return keccak256(abi.encode(block.timestamp));
}
function setTokenPool(address _token, address _pool) external {
tokenPools[_token] = _pool;
}
+ error InvalidExtraArgsTag();
+ function _fromBytes(bytes calldata extraArgs) internal view returns (Client.EVMExtraArgsV1 memory) {
+ if (extraArgs.length == 0) {
+ return Client.EVMExtraArgsV1({gasLimit: 123, strict: false});
+ }
+ if (bytes4(extraArgs) != Client.EVM_EXTRA_ARGS_V1_TAG) revert InvalidExtraArgsTag();
+ return abi.decode(extraArgs[4:], (Client.EVMExtraArgsV1));
+ }

Hardhat tests will revert with Error: VM Exception while processing transaction: reverted with custom error 'InvalidExtraArgsTag()'

Recommendations

Encode extraArgs properly before sending the message as shown in the ChainLink CCIP starter kit:

extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000, strict: false}) // Additional arguments, setting gas limit and non-strict sequency mode
),
Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.