First Flight #12: Kitty Connect

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

Token ID accounting error in `KittyConnect::bridgeNftToAnotherChain()` due to logic flaw.

Summary

The KittyConnect::bridgeNftToAnotherChain() function facilitates cross-chain token transfers. However, there is a flaw wherein,
after burning a specific token, the function erroneously counts its ID as still valid for transfer.

This issue arises from the utilization of mapping(address user => uint256[]) private s_ownerToCatsTokenId to monitor token IDs per user.
The problem occurs when bridgeNftToAnotherChain recalculates the IDs (using the idx variable in the mapping) post-token burn,
resulting in incorrect IDs being assigned to the tokens the user retains.

function bridgeNftToAnotherChain(uint64 destChainSelector, address destChainBridge, uint256 tokenId) external {
...
//@Audit: Here recalculates the new idx for the tokens
uint256[] memory userTokenIds = s_ownerToCatsTokenId[msg.sender];
uint256 lastItem = userTokenIds[userTokenIds.length - 1];
s_ownerToCatsTokenId[msg.sender].pop();
if (idx < (userTokenIds.length - 1)) {
s_ownerToCatsTokenId[msg.sender][idx] = lastItem;
}
...
}

Vulnerability Details

  • A test is being implemented to expose a logic flaw. In this scenario, a shop partner mints tokens with IDs 0, 1, and 2 for a specific user.

  • Subsequently, the user attempts to transfer tokens with IDs 0 and 2, but after the transfers, retains token ID 2 instead of 1, which was not transferred.

  • To facilitate testing, a mock of the bridgeNftToAnotherChain function is employed. Two lines pertaining to the bridge transfer are commented out.

function mock_bridgeNftToAnotherChain(uint64 destChainSelector, address destChainBridge, uint256 tokenId) external {
address catOwner = _ownerOf(tokenId);
require(msg.sender == catOwner);
CatInfo memory catInfo = s_catInfo[tokenId];
uint256 idx = catInfo.idx;
@> // bytes memory data = abi.encode(catOwner, catInfo.catName, catInfo.breed, catInfo.image, catInfo.dob, catInfo.shopPartner);
_burn(tokenId);
delete s_catInfo[tokenId];
uint256[] memory userTokenIds = s_ownerToCatsTokenId[msg.sender];
uint256 lastItem = userTokenIds[userTokenIds.length - 1];
s_ownerToCatsTokenId[msg.sender].pop();
if (idx < (userTokenIds.length - 1)) {
s_ownerToCatsTokenId[msg.sender][idx] = lastItem;
}
emit NFTBridgeRequestSent(block.chainid, destChainSelector, destChainBridge, tokenId);
@> // i_kittyBridge.bridgeNftWithData(destChainSelector, destChainBridge, data);
}
function test_bridgeNftToAnotherChain() public {
string memory catImageIpfsHash = "ipfs://QmbxwGgBGrNdXPm84kqYskmcMT3jrzBN8LzQjixvkz4c62";
vm.startPrank(partnerA);
kittyConnect.mintCatToNewOwner(user, catImageIpfsHash, "Cat1", "Ragdoll", block.timestamp);
kittyConnect.mintCatToNewOwner(user, catImageIpfsHash, "Cat2", "Ragdoll", block.timestamp);
kittyConnect.mintCatToNewOwner(user, catImageIpfsHash, "Cat3", "Ragdoll", block.timestamp);
vm.stopPrank();
vm.startPrank(user);
console.log("::Tokens before all transfers");
uint256[] memory idsBefore = kittyConnect.getCatsTokenIdOwnedBy(user);
for (uint256 i = 0; i < idsBefore.length; i++) {
console.log("Token : ", idsBefore[i]);
}
//Transfer token 0
kittyConnect.mock_bridgeNftToAnotherChain(uint64(0), address(0), 0);
console.log("::Tokens after transfering token 0");
uint256[] memory idsAfterTransferOne = kittyConnect.getCatsTokenIdOwnedBy(user);
for (uint256 i = 0; i < idsAfterTransferOne.length; i++) {
console.log("Token : ", idsAfterTransferOne[i]);
}
//Transfer token 2
kittyConnect.mock_bridgeNftToAnotherChain(uint64(0), address(0), 2);
console.log("::Tokens after transfering token 2");
uint256[] memory idsAfterTransferTwo = kittyConnect.getCatsTokenIdOwnedBy(user);
for (uint256 i = 0; i < idsAfterTransferTwo.length; i++) {
console.log("Token : ", idsAfterTransferTwo[i]);
}
vm.stopPrank();
}

Output

Logs:
::Tokens before all transfers
Token : 0
Token : 1
Token : 2
::Tokens after transfering token 0
Token : 2
Token : 1
::Tokens after transfering token 2
Token : 2

Impact

In some cases, incorrect token IDs are attributed to the user after a transfer to another chain.

Tools Used

Foundry and manual review

Recommendations

Revise the token ID accounting mechanism (idx) to accurately reflect the transferred IDs.

function bridgeNftToAnotherChain(uint64 destChainSelector, address destChainBridge, uint256 tokenId) external {
address catOwner = _ownerOf(tokenId);
require(msg.sender == catOwner);
CatInfo memory catInfo = s_catInfo[tokenId];
uint256 idx = catInfo.idx;
bytes memory data = abi.encode(catOwner, catInfo.catName, catInfo.breed, catInfo.image, catInfo.dob, catInfo.shopPartner);
_burn(tokenId);
delete s_catInfo[tokenId];
-- uint256[] memory userTokenIds = s_ownerToCatsTokenId[msg.sender];
-- uint256 lastItem = userTokenIds[userTokenIds.length - 1];
-- s_ownerToCatsTokenId[msg.sender].pop();
-- if (idx < (userTokenIds.length - 1)) {
-- s_ownerToCatsTokenId[msg.sender][idx] = lastItem;
-- }
++ //Array Remove An Element By Replacing Last
++ uint256[] storage userTokenIds = s_ownerToCatsTokenId[msg.sender];
++ uint256 lastTokenId = userTokenIds[userTokenIds.length - 1];
++ userTokenIds[idx] = lastTokenId;
++ s_catInfo[lastTokenId].idx = idx;
++ userTokenIds.pop();
emit NFTBridgeRequestSent(block.chainid, destChainSelector, destChainBridge, tokenId);
i_kittyBridge.bridgeNftWithData(destChainSelector, destChainBridge, data);
}
Updates

Lead Judging Commences

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

Consider updating index of an NFT when making a transfer to another user or chain

Support

FAQs

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