DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Soulbound NFT Can Still Be Transferred

Root + Impact

Description

Normal behavior:
SoulboundProfileNFT is intended to be an ERC721 soulbound, so it cannot be moved in any way once minted. The profile identity must remain permanently tied to one address.

Issues:
The contract does not override the entire ERC721 transfer path. One of the safeTransferFrom overloads is still active, so tokens can still be transferred, making the NFTs not truly soulbound and violating the identity invariant.

// Root cause: missing override for one safeTransferFrom overload
// @> Only this overload is overridden
function safeTransferFrom(address, address, uint256, bytes memory) public pure override {
revert SoulboundTokenCannotBeTransferred();
}
// @> MISSING override for:
// function safeTransferFrom(address from, address to, uint256 tokenId)

Risk

Likelihood:

  • Reason 1: Any ERC721 holder can call safeTransferFrom(address,address,uint256) directly, because this function is publicly available from OpenZeppelin ERC721.

  • Reason 2: The transfer will be successful because there is no revert to the overload.

Impact:

  • Impact 1: The profile NFT can be moved to another address, so it is no longer soulbound.

  • Impact 2: The “1 address = 1 immutable profile identity” invariant is broken, potentially breaking identity assumptions in other contracts that rely on this NFT.

Proof of Concept

Explanation:
SoulboundProfileNFT only partially blocks ERC721 transfer paths.
Even though transferFrom and one overload safeTransferFrom have been overridden to always revert, the other overload safeTransferFrom(address,address,uint256) is still available from the OpenZeppelin ERC721 implementation and is not blocked.

As a result, the token owner can still move the profile NFT using the overload, so the token is not truly soulbound and the identity invariant is broken.

// Assume userA has successfully minted a profile NFT with tokenId
// Call the unoverridden ERC721 overload
soulboundProfileNFT.safeTransferFrom(userA, userB, tokenId);
// Result:
// - Transaction succeeds
// - Ownership of tokenId moves from userA to userB
// - This violates the intended soulbound property of the profile NFT

Why This Works:

  • OpenZeppelin ERC721 exposes two overloads of safeTransferFrom

  • The contract only overrides:

safeTransferFrom(address, address, uint256, bytes)
  • The overload below remains callable and does not revert:

safeTransferFrom(address, address, uint256)

Recommended Mitigation

To effectively make the NFT soulbound, all transfer paths (including both safeTransferFrom overloads and transferFrom) must be blocked. Since the contract uses OpenZeppelin v5.x logic, the most robust way to do this is by overriding the internal _update function:

/**
* @dev See {ERC721-_update}.
* Reverts for any transfer that is not a mint or a burn.
*/
function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
address from = _ownerOf(tokenId);
if (from != address(0) && to != address(0)) {
revert SoulboundTokenCannotBeTransferred();
}
return super._update(to, tokenId, auth);
}

Note: This single override replaces the need for manual overrides of transferFrom and both safeTransferFrom overloads.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!