First Flight #12: Kitty Connect

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

After the ownership of a cat is transferred, the previous owner still owns the cat's `tokenId`

Summary

The KittyConnect contract allows users to mint NFTs representing ownership of cats. The safeTransferFrom function is used to transfer the ownership of a given cat. But there is an issue in this function that leads to ownership inconsistencies.

Vulnerability Details

The KittyConnect::safeTransferFrom frunction transfers the cat ownership to a new owner:

function safeTransferFrom(address currCatOwner, address newOwner, uint256 tokenId, bytes memory data) public override onlyShopPartner {
require(_ownerOf(tokenId) == currCatOwner, "KittyConnect__NotKittyOwner");
require(getApproved(tokenId) == newOwner, "KittyConnect__NewOwnerNotApproved");
_updateOwnershipInfo(currCatOwner, newOwner, tokenId);
emit CatTransferredToNewOwner(currCatOwner, newOwner, tokenId);
_safeTransfer(currCatOwner, newOwner, tokenId, data);
}

The function calls the internal function _updateOwnershipInfo to update the information about the new and prevous owners:

function _updateOwnershipInfo(address currCatOwner, address newOwner, uint256 tokenId) internal {
s_catInfo[tokenId].prevOwner.push(currCatOwner);
s_catInfo[tokenId].idx = s_ownerToCatsTokenId[newOwner].length;
s_ownerToCatsTokenId[newOwner].push(tokenId);
}

Actually, this function adds the current owner to the prevOwner array of the cat and adds the tokenId to the newOwner address. But the function doesn't remove the tokenId from the current owner's collection of tokenIds. That means, the current owner is still owner of the cat.

Impact

The safeTransferFrom function does not remove the token from the s_ownerToCatsTokenId mapping of the current owner after a successful transfer. This results in the previous owner still having a reference to a token they no longer own.
The test function test__safetransferCatToNewOwner function in KittyTest.t.sol file demontrates this issue. You can execute this test using the foundry command: forge test --match-test "test_safetransferCatToNewOwner" -vvvvv.

function test_safetransferCatToNewOwner() public {
string memory catImageIpfsHash = "ipfs://QmbxwGgBGrNdXPm84kqYskmcMT3jrzBN8LzQjixvkz4c62";
uint256 tokenId = kittyConnect.getTokenCounter();
address newOwner = makeAddr("newOwner");
vm.prank(partnerA);
kittyConnect.mintCatToNewOwner(user, catImageIpfsHash, "Meowdy", "Ragdoll", block.timestamp);
vm.prank(user);
kittyConnect.approve(newOwner, tokenId);
vm.expectEmit(false, false, false, true, address(kittyConnect));
emit CatTransferredToNewOwner(user, newOwner, tokenId);
vm.prank(partnerA);
kittyConnect.safeTransferFrom(user, newOwner, tokenId);
assertEq(kittyConnect.ownerOf(tokenId), newOwner);
//@audit the tokenId of the cat is owned also by the old owner
console.log(kittyConnect.getCatsTokenIdOwnedBy(user).length);
assertEq(kittyConnect.getCatsTokenIdOwnedBy(user).length, 0); //owned by user returns 1
assertEq(kittyConnect.getCatsTokenIdOwnedBy(newOwner).length, 1);
assertEq(kittyConnect.getCatsTokenIdOwnedBy(newOwner)[0], tokenId);
assertEq(kittyConnect.getCatInfo(tokenId).prevOwner[0], user);
assertEq(kittyConnect.getCatInfo(tokenId).prevOwner.length, 1);
assertEq(kittyConnect.getCatInfo(tokenId).idx, 0);
}

The test fails due to this line: assertEq(kittyConnect.getCatsTokenIdOwnedBy(user).length, 0);. The user is the prevous owner and should have 0 cats tokenId, but the protocol doesn't update the tokens owned by the user after the transfer ownership of the cat. Therefore, the prevous owner still owns 1 catsTokenId:

Ran 1 test for test/KittyTest.t.sol:KittyTest
[FAIL. Reason: assertion failed: 1 != 0] test_safetransferCatToNewOwner() (gas: 418363)
Logs:
1

Tools Used

Manual Review, Foundry

Recommendations

Implement a function to remove the token from the prevous owner:

+ function _removeTokenFromPreviousOwner(address prevOwner, uint256 tokenId) internal {
+ uint256 lastTokenIndex = s_ownerToCatsTokenId[prevOwner].length - 1;
+ uint256 tokenIndex = s_catInfo[tokenId].idx;
+ // Only need to swap if the token to remove is not the last one
+ if (tokenIndex != lastTokenIndex) {
+ uint256 lastTokenId = s_ownerToCatsTokenId[prevOwner][lastTokenIndex];
+ // Move the last token to the slot of the token to remove
+ s_ownerToCatsTokenId[prevOwner][tokenIndex] = lastTokenId;
+ // Update the moved token's index
+ s_catInfo[lastTokenId].idx = tokenIndex;
+ }
+ // Remove the last token (which is now the token to remove)
+ s_ownerToCatsTokenId[prevOwner].pop();
+ }

Then calls this function from the KittyConnect::_updateOwnershipInfo function:

function _updateOwnershipInfo(address currCatOwner, address newOwner, uint256 tokenId) internal {
s_catInfo[tokenId].prevOwner.push(currCatOwner);
s_catInfo[tokenId].idx = s_ownerToCatsTokenId[newOwner].length;
s_ownerToCatsTokenId[newOwner].push(tokenId);
//@audit add a function to remove the tokenId from the currCatOwner
+ _removeTokenFromPreviousOwner(currCatOwner, tokenId);
}
Updates

Lead Judging Commences

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

Improper token ownership update in `_updateOwnershipInfo`

Support

FAQs

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