Summary
Users are unable to bridge their nft to another chain from the KittyConnect
through bridgeNftToAnotherChain()
or directly on KittyBridge
through bridgeNftWithData()
.
Vulnerability Details
When a user tries to bridge an nft, the user will get an error [FAIL. Reason: revert: SafeERC20: low-level call failed]
On further inspection, i discovered that the bridgeNftWithData()
does not implement approval for the router
so that the router
will be able to send the fee required to send the CCIP message.
function bridgeNftWithData(uint64 _destinationChainSelector, address _receiver, bytes memory _data)
external
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(_receiver, _data, address(s_linkToken));
IRouterClient router = IRouterClient(this.getRouter());
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
All users are unable to bridge their nfts to other chains
POC
function test_bridgeNft() public {
address sender = makeAddr("sender");
uint64 chainId = 16015286601757825753;
bytes memory data = abi.encode(
makeAddr("catOwner"),
"meowdy",
"ragdoll",
"ipfs://QmbxwGgBGrNdXPm84kqYskmcMT3jrzBN8LzQjixvkz4c62",
block.timestamp,
partnerA
);
vm.prank(address(kittyConnectOwner));
kittyBridge.allowlistDestinationChain(chainId, true);
vm.prank(sender);
vm.expectRevert();
kittyBridge.bridgeNftWithData(chainId, sender, data);
}
Tools Used
VS Code, Foundry, Manual Review
Recommendations
Add the approval of link tokens for the router to the bridgeNftWithData()
function bridgeNftWithData(uint64 _destinationChainSelector, address _receiver, bytes memory _data)
external
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// @audit attacker can mint nfts
// 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);
}
// approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
+ s_linkToken.approve(address(router), fees);
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
emit MessageSent(messageId, _destinationChainSelector, _receiver, _data, address(s_linkToken), fees);
return messageId;
}