DatingDapp

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

SoulboundProfileNFT does not override approve() or setApprovalForAll() — approval mechanism remains active on a non-transferable token

Root + Impact

Description

// SoulboundProfileNFT.sol
// @> transferFrom is correctly blocked
function transferFrom(address, address, uint256) public pure override {
revert SoulboundTokenCannotBeTransferred();
}
// @> safeTransferFrom is correctly blocked
function safeTransferFrom(address, address, uint256, bytes memory) public pure override {
revert SoulboundTokenCannotBeTransferred();
}
// @> approve() is NOT overridden — inherited from ERC721, fully functional
// function approve(address to, uint256 tokenId) public virtual override { ... }
// @> setApprovalForAll() is NOT overridden — inherited from ERC721, fully functional
// function setApprovalForAll(address operator, bool approved) public virtual override { ... }

Risk

Likelihood:

  • Any user interacting with a standard ERC-721 wallet interface will see the approve and setApprovalForAll options available — standard UX flows surface these buttons.

  • Phishing sites targeting NFT holders routinely request setApprovalForAll signatures. Even though the transfer will fail, users may not know this and could be deceived.

Impact:

  • Profile NFTs can be "approved" to third-party addresses, creating misleading on-chain state that contradicts the soulbound design.

  • Third-party tooling that reads approval state (OpenSea, Blur, wallet dashboards) may display these profiles as transferable, confusing users and undermining trust in the protocol's identity guarantees.

  • Phishing vectors exist: users tricked into signing approvals believe they've compromised their profile.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
contract SoulboundApprovalTest is Test {
SoulboundProfileNFT nft;
address alice = makeAddr("alice");
address attacker = makeAddr("attacker");
function setUp() public {
nft = new SoulboundProfileNFT();
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
}
function test_approveStillWorks() public {
uint256 tokenId = nft.profileToToken(alice);
// approve() is NOT blocked — attacker gets approved on a "soulbound" token
vm.prank(alice);
nft.approve(attacker, tokenId); // @> does not revert
assertEq(nft.getApproved(tokenId), attacker);
}
function test_setApprovalForAllStillWorks() public {
// setApprovalForAll() is NOT blocked
vm.prank(alice);
nft.setApprovalForAll(attacker, true); // @> does not revert
assertTrue(nft.isApprovedForAll(alice, attacker));
}
function test_transferStillBlocked() public {
uint256 tokenId = nft.profileToToken(alice);
vm.prank(alice);
nft.approve(attacker, tokenId);
// Transfer still reverts even with approval — but approval state was set
vm.prank(attacker);
vm.expectRevert(SoulboundProfileNFT.SoulboundTokenCannotBeTransferred.selector);
nft.transferFrom(alice, attacker, tokenId);
}
}

Recommended Mitigation

Override approve() and setApprovalForAll() to revert with the same custom error:

// SoulboundProfileNFT.sol — add these two overrides
/// @notice Override to prevent approval of soulbound tokens
function approve(address, uint256) public pure override {
revert SoulboundTokenCannotBeTransferred();
}
/// @notice Override to prevent operator approval of soulbound tokens
function setApprovalForAll(address, bool) public pure override {
revert SoulboundTokenCannotBeTransferred();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!