I've been working to adapt the Transfer USDC With Data tutorial from the Chainlink Documentation to my project and this is driving me mad. I initially got the sender contract to send USDC to my receiver contract and the receiver contract sent it to my "staker" equivalent contract, a PrizeVault contract, but it didn't trigger the deposit function selector. So I made a change and now my CCIP transactions fail. They get blessed, committed, and then I look back and I need to do a manual gas transaction. Even looking at my deployed Receiver contract, there are no failed messages so it looks like the destination router contract doesn't get the message to my receiver contract. I don't remember the change I made to regress to this point.
EDIT: now that I recall, I think I changed my function selector to match my deposit function selector in building the CCIP message as it was previously the unchanged selector from the tutorial.
Relevant functions on Sender contract on Avalanche Fuji
function crossChainDepositPayLink( uint64 _destinationChainSelector, address _beneficiary, // beneficiary of the staked tokens on the destination chain uint256 _amount ) external onlyOwner validateDestinationChain(_destinationChainSelector) returns (bytes32 messageId) { address receiver = s_receivers[_destinationChainSelector]; if (receiver == address(0)) revert NoReceiverOnDestinationChain(_destinationChainSelector); if (_amount == 0) revert AmountIsZero(); uint256 gasLimit = s_gasLimits[_destinationChainSelector]; if (gasLimit == 0) revert NoGasLimitOnDestinationChain(_destinationChainSelector); // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( receiver, abi.encodeWithSelector( IPrizeVault.deposit.selector, _amount, _beneficiary ), // Encode the function selector and the arguments of the stake function), i_payInLinkOrGasToken[true], // paying in Link gasLimit, true, address(i_usdcToken), _amount ); // Get the fee required to send the CCIP message uint256 fees = i_router.getFee( _destinationChainSelector, evm2AnyMessage ); if (fees > i_linkToken.balanceOf(address(this))) revert NotEnoughBalance(i_linkToken.balanceOf(address(this)), fees); // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK i_linkToken.approve(address(i_router), fees); // approve the Router to spend usdc tokens on contract's behalf. It will spend the amount of the given token i_usdcToken.approve(address(i_router), _amount); // Send the message through the router and store the returned message ID messageId = i_router.ccipSend( _destinationChainSelector, evm2AnyMessage ); // Emit an event with message details emit MessageSent( messageId, _destinationChainSelector, receiver, _beneficiary, address(i_usdcToken), _amount, address(i_linkToken), fees ); // Return the message ID return messageId; } ^^^^ I put a bool -> address mapping of gas tokens in i_payInLinkOrGasToken. True is Link's token address while false is address(0) to specify using the gas token. I built the CCIP Message in the contract below. In the full contract, I have two named _buildCCIPMessage functions but one has more parameters so the latter is used for token transfers. The signature would be different so I don't anticipate this to cause an error.
function _buildCCIPMessage( // I can use the same function name with extra parameters and it'll recognize as unique address _receiver, bytes memory _data, address _feeTokenAddress, uint256 _gasLimit, bool _allowOutOfOrderExecution, address _token, uint256 _amount ) private pure returns (Client.EVM2AnyMessage memory){ // setting token amounts: // If not sending any token, create an empty array, if yes, create 1 size array Client.EVMTokenAmount[] memory tokenAmounts = _token == address(0) || _amount == 0 ? new Client.EVMTokenAmount[](0) : new Client.EVMTokenAmount[](1); if (tokenAmounts.length > 0){ tokenAmounts[0] = Client.EVMTokenAmount({ token: _token, amount: _amount }); } // Create EVM2AnyMessage struct in memorry with necessary info for sending cross chain message return Client.EVM2AnyMessage({ receiver: abi.encode(_receiver), // ABI-encoded receiver address data: _data, // abi.encode(_text) -> BI-encoded string tokenAmounts: tokenAmounts, extraArgs: Client._argsToBytes( Client.GenericExtraArgsV2({ gasLimit: _gasLimit, // 200_000 - gas limit for callback on destination chain allowOutOfOrderExecution: _allowOutOfOrderExecution // true }) ), feeToken: _feeTokenAddress }); } Relevant functions for Sepolia's Receiver contract
constructor( address _router, address _usdcToken, address _staker ) CCIPReceiver(_router) { if (_usdcToken == address(0)) revert InvalidUsdcToken(); if (_staker == address(0)) revert InvalidStaker(); i_usdcToken = IERC20(_usdcToken); i_staker = _staker; i_usdcToken.safeApprove(_staker, type(uint256).max); //@audit-info dunno if I need this for the main contract } ^^^^ The constructor is posted ad verbatim from the tutorial. Just adding this to show it's not an ERC-20 approval issue. And I passed the PrizeVault contract to the _staker constructor parameter since the variable is already there.
function ccipReceive( Client.Any2EVMMessage calldata any2EvmMessage ) external override onlyRouter { // validate the sender contract if ( abi.decode(any2EvmMessage.sender, (address)) != s_senders[any2EvmMessage.sourceChainSelector] ) revert WrongSenderForSourceChain(any2EvmMessage.sourceChainSelector); /* solhint-disable no-empty-blocks */ try this.processMessage(any2EvmMessage) { // Intentionally empty in this example; no action needed if processMessage succeeds } catch (bytes memory err) { // Could set different error codes based on the caught error. Each could be // handled differently. s_failedMessages.set( any2EvmMessage.messageId, uint256(ErrorCode.FAILED) ); s_messageContents[any2EvmMessage.messageId] = any2EvmMessage; // Don't revert so CCIP doesn't revert. Emit event instead. // The message can be retried later without having to do manual execution of CCIP. emit MessageFailed(any2EvmMessage.messageId, err); return; } } ^^^^ No changes that I recall again.
/// @notice Serves as the entry point for this contract to process incoming messages. /// @param any2EvmMessage Received CCIP message. /// @dev Transfers specified token amounts to the owner of this contract. This function /// must be external because of the try/catch for error handling. /// It uses the `onlySelf`: can only be called from the contract. function processMessage( Client.Any2EVMMessage calldata any2EvmMessage ) external onlySelf { _ccipReceive(any2EvmMessage); // process the message - may revert } function _ccipReceive( Client.Any2EVMMessage memory any2EvmMessage ) internal override { if (any2EvmMessage.destTokenAmounts[0].token != address(i_usdcToken)) revert WrongReceivedToken( address(i_usdcToken), any2EvmMessage.destTokenAmounts[0].token ); (bool success, bytes memory returnData) = i_staker.call( any2EvmMessage.data ); // low level call to the staker contract using the encoded function selector and arguments if (!success) revert CallToStakerFailed(); if (returnData.length > 0) revert NoReturnDataExpected(); emit MessageReceived( any2EvmMessage.messageId, any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector) abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address, any2EvmMessage.data, // received data any2EvmMessage.destTokenAmounts[0].token, any2EvmMessage.destTokenAmounts[0].amount ); } ^^^ No changes that I can recall again from the tutorial.
PrizeVault contract
/// @inheritdoc IERC4626 function deposit(uint256 _assets, address _receiver) external returns (uint256) { uint256 _shares = previewDeposit(_assets); _depositAndMint(msg.sender, _receiver, _assets, _shares); return _shares; } ^^^^ It works when I call the contract directly on the chain. So I'm only posting it to show the function signature. One thing I did not do that the tutorial did was create an interface *in the same [PrizeVault] contract file.
I am at a loss here so any help would be appreciated. Thank you.
EDIT:
interface IPrizeVault { function deposit(uint256 _assets, address _receiver) external; } PrizeVault interface that I use for the selector in the internal function _buildCCIPMessage. It's in my Sender file but it's defined outside of the Sender contract.