First Flight #12: Kitty Connect

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

Calling `KittyBridge::bridgeNftWithData` should failed becase missing `approve` function to pay CCIP fees

Summary

The KittyBridge::bridgeNftWithData function missing IERC20::approve for router, making bridgeNftWithData fails as router would never able to send fees

Vulnerability Details

when using KittyConnect::bridgeNftToAnotherChain after the proper checks pass, it would call KittyBridge::bridgeNftWithData function but this particular function should fail because after it calculate the fee required, router instance would fail to send the LINK as fee on KittyBridge behalf because router does not have sufficient approval to spend LINK owned by KittyBridge contract.

KittyBridge.sol:

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;
}
proof of code

add SEPOLIA_RPC_URL and FUJI_RPC_URL to .env file using value from infura/alchemy rpc url.

add this contract in test folder:

BridgeTest.t.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {DeployKittyConnect} from "../script/DeployKittyConnect.s.sol";
import {HelperConfig} from "../script/HelperConfig.s.sol";
import {KittyConnect} from "../src/KittyConnect.sol";
import {KittyBridge, KittyBridgeBase, Client} from "../src/KittyBridge.sol";
import {IERC20} from
"@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/IERC20.sol";
contract BridgeTest is Test {
// sepolia as main chain
KittyConnect kittyConnect;
KittyBridge kittyBridge;
HelperConfig helperConfig;
HelperConfig.NetworkConfig networkConfig;
// fuji as other chain
KittyConnect kittyConnectFuji;
KittyBridge kittyBridgeFuji;
HelperConfig helperConfigFuji;
HelperConfig.NetworkConfig networkConfigFuji;
address kittyConnectOwner;
address partnerA;
address partnerB;
address user;
uint256 fujiFork;
uint256 sepoliaFork;
string FUJI_RPC_URL = vm.envString("FUJI_RPC_URL");
string SEPOLIA_RPC_URL = vm.envString("SEPOLIA_RPC_URL");
address linkTokenSepolia;
address linkTokenFuji;
function setUp() external {
fujiFork = vm.createFork(FUJI_RPC_URL);
sepoliaFork = vm.createFork(SEPOLIA_RPC_URL);
// FUJI CHAIN
vm.selectFork(fujiFork);
// fund address with LINK (Fuji)
linkTokenFuji = 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846;
deal(linkTokenFuji, msg.sender, 10 ether);
// deploy to fuji
// we only need KittyConnectFuji and KittyBridgeFuji
DeployKittyConnect deployerFuji = new DeployKittyConnect();
(kittyConnectFuji, helperConfigFuji) = deployerFuji.run();
kittyBridgeFuji = KittyBridge(kittyConnectFuji.getKittyBridge());
// SEPOLIA CHAIN
vm.selectFork(sepoliaFork);
console.log(block.chainid);
// fund address with LINK (Sepolia)
linkTokenSepolia = 0x779877A7B0D9E8603169DdbD7836e478b4624789;
deal(linkTokenSepolia, msg.sender, 10 ether);
// deploy sepolia
DeployKittyConnect deployer = new DeployKittyConnect();
(kittyConnect, helperConfig) = deployer.run();
networkConfig = helperConfig.getNetworkConfig();
kittyConnectOwner = kittyConnect.getKittyConnectOwner();
partnerA = kittyConnect.getKittyShopAtIdx(0);
partnerB = kittyConnect.getKittyShopAtIdx(1);
kittyBridge = KittyBridge(kittyConnect.getKittyBridge());
}
function test_bridgeNftToAnotherChainFromSepoliaToFuji() public {
address receiver = makeAddr("receiver");
string memory catImageIpfsHash = "ipfs://QmbxwGgBGrNdXPm84kqYskmcMT3jrzBN8LzQjixvkz4c62";
// partnerA mints NFT
vm.prank(partnerA);
kittyConnect.mintCatToNewOwner(receiver, catImageIpfsHash, "Meowdy", "Ragdoll", block.timestamp);
uint256[] memory usersTokenId = kittyConnect.getCatsTokenIdOwnedBy(receiver);
// receiver wants to bridge their NFT from sepolia to fuji
vm.prank(receiver);
kittyConnect.bridgeNftToAnotherChain(networkConfig.otherChainSelector, address(kittyBridgeFuji), usersTokenId[0]);
}
}

then run forge test --mt test_bridgeNftToAnotherChainFromSepoliaToFuji and the result should FAIL:

Failing tests:
Encountered 1 failing test in test/BridgeTest.t.sol:BridgeTest
[FAIL. Reason: revert: ERC20: transfer amount exceeds allowance] test_bridgeNftToAnotherChainFromSepoliaToFuji() (gas: 391259)

Impact

Protocol would be unable to bridge NFTs because it cannot pay for fees needed to send the CCIP message.

Tools Used

foundry and manual review

Recommendations

after checking if KittyBridge contract have sufficient fees to send the CCIP message, there should be a line to approve the router to send LINK token on behalf of KittyBridge before router.ccipSend is called.

add the following code to KittyBridge.sol:

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;
}

then run forge test --mt test_bridgeNftToAnotherChainFromSepoliaToFuji and the result should PASS:

Ran 1 test for test/BridgeTest.t.sol:BridgeTest
[PASS] test_bridgeNftToAnotherChainFromSepoliaToFuji() (gas: 374880)
Updates

Lead Judging Commences

inallhonesty Lead Judge over 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.