Summary
Anyone can call KittyBridge::bridgeNftWithData()
allowing them to mint a NFT on a different chain without actually owning a NFT provided by a shop partner.
Vulnerability Details
The KittyBridge::bridgeNftWithData()
function is responsible for building and sending 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;
}
The ideal scenario would be that this function gets called from the KittyConnect::bridgeNftToAnotherChain()
function since the function verifies the owner of the NFT, and burns the NFT, before finally calling the KittyBridge::bridgeNftWithData()
with the appropriate data of the cat NFT.
However since KittyBridge::bridgeNftWithData()
is external and only checks for onlyAllowlistedDestinationChain, and validateReceiver, any contract or account can call this function passing in any arbitrary data to represent a cat NFT that isn't provided by a shop partner.
Impact
Tools Used
VS Code, Foundry
Recommendations
Create a onlyKittyConnect
modifier and add it to the KittyBridge::bridgeNftWithData()
function:
+ modifier onlyKittyConnect() {
+ require(
+ msg.sender == kittyConnect,
+ "KittyConnect__NotKittyConnect"
+ );
+ _;
+ }
function bridgeNftWithData(uint64 _destinationChainSelector, address _receiver, bytes memory _data)
external
+ onlyKittyConnect()
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;
}