First Flight #12: Kitty Connect

First Flight #12: Kitty Connect
Beginner FriendlyFoundryNFTGameFi
100 EXP
View results
Submission Details
Severity: high
Valid

Missing Link Token Approval in `KittyBridge::bridgeNftWithData` Making Transaction to Revert

[H-1] Missing Link Token Approval in KittyBridge::bridgeNftWithData Making Transaction to Revert

Description: The KittyBridge::bridgeNftWithData function is designed to facilitate the transfer of NFTs across different blockchain networks. However, it does not explicitly approve the Chainlink CCIP Router to take fees in the form of LINK tokens.

The ChainLink CCIP Example:
.
.
.
function sendMessage(
uint64 destinationChainSelector,
address receiver,
string calldata text
) external onlyOwner returns (bytes32 messageId) {
.
.
.
if (fees > s_linkToken.balanceOf(address(this)))
revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
// approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
@> s_linkToken.approve(address(s_router), fees);
// Send the message through the router and store the returned message ID
messageId = s_router.ccipSend(destinationChainSelector, evm2AnyMessage);
.
.
.
}
Kitty Bridge Function:
function bridgeNftWithData(
uint64 _destinationChainSelector,
address _receiver,
bytes memory _data
)
external
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_data,
address(s_linkToken)
);
// Initialize a router client instance to interact with cross-chain router
IRouterClient router = IRouterClient(this.getRouter());
// Get the fee required to send the CCIP message
uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
if (fees > s_linkToken.balanceOf(address(this))) {
revert KittyBridge__NotEnoughBalance(
s_linkToken.balanceOf(address(this)),
fees
);
}
@>
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_data,
address(s_linkToken),
fees
);
return messageId;
}

Impact: This oversight leads to the transaction reverting when the CCIP Router attempts to deduct fees, as it lacks the necessary approval to do so.

Proof of Concept: To demonstrate this issue, one can attempt to execute the bridgeNftWithData function without first approving the CCIP Router to take LINK tokens. Add the following test to project test suit.

function test_callingBridgeNftWithDataFailsWithoutApproval()
public
partnerGivesCatToOwner
{
////fund the kittyBridge with link
uint256 InitialLink = 10_000_000_000_000_000_000;
address linkHolder = 0x61E5E1ea8fF9Dc840e0A549c752FA7BDe9224e99; //address of linkHolder on sepolia testnet
vm.prank(linkHolder);
linkToken.approve(address(kittyBridge), InitialLink + 1);
vm.prank(address(kittyBridge));
linkToken.transferFrom(linkHolder, address(kittyBridge), InitialLink);
vm.startPrank(user);
uint256 tokenId = kittyConnect.getTokenCounter();
uint64 destChainSelector = 14767482510784806043;
vm.expectRevert("ERC20: transfer amount exceeds allowance");
kittyConnect.bridgeNftToAnotherChain(
destChainSelector,
address(kittyBridge),
tokenId - 1 //tokenId 1
);
}

Recommended Mitigation: To mitigate this issue, the KittyBridge::bridgeNftWithData function should be modified to include a step where it approves the CCIP Router to take a certain amount of LINK tokens as fees.

function bridgeNftWithData(
uint64 _destinationChainSelector,
address _receiver,
bytes memory _data
)
external
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_data,
address(s_linkToken)
);
// Initialize a router client instance to interact with cross-chain router
IRouterClient router = IRouterClient(this.getRouter());
// Get the fee required to send the CCIP message
uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
if (fees > s_linkToken.balanceOf(address(this))) {
revert KittyBridge__NotEnoughBalance(
s_linkToken.balanceOf(address(this)),
fees
);
}
+ s_linkToken.approve(address(router), fees);
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_data,
address(s_linkToken),
fees
);
return messageId;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Missing fee token approval

Support

FAQs

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