DatingDapp

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

blocked user retain existing multisig control

Root + Impact

Description

The app owner can block a user by burning their soulbound profile NFT. Blocking removes the user's current profile, and future calls to likeUser() will fail unless the user has a profile NFT.

Existing MultiSigWallet contracts do not check the profile NFT status of their owners. Once a matched user is set as owner1 or owner2, that address can continue submitting, approving, and executing multisig transactions even after the app owner blocks the profile.

function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
// @> Blocking only burns the profile NFT and deletes profile storage
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
modifier onlyOwners() {
// @> MultiSig authorization only checks fixed owner addresses
// @> It does not check whether the owner still has a valid profile NFT
if (msg.sender != owner1 && msg.sender != owner2) revert NotAnOwner();
_;
}
function submitTransaction(address _to, uint256 _value) external onlyOwners {
if (_to == address(0)) revert InvalidRecipient();
if (_value == 0) revert InvalidAmount();
transactions.push(Transaction(_to, _value, false, false, false));
uint256 txId = transactions.length - 1;
emit TransactionCreated(txId, _to, _value);
}

Risk

Likelihood:

  • This occurs when a user is blocked after already being assigned as an owner of a match multisig.

  • This occurs because MultiSigWallet stores static owner addresses and has no link back to the profile registry.

Impact:

  • A blocked user retains control over previously created match wallets.

  • Blocking does not fully revoke app-level privileges where multisig access is part of the dating app's reward flow.

Proof of Concept

function testBlockedUserCanStillControlExistingMultiSig() public {
SoulboundProfileNFT nft = new SoulboundProfileNFT();
address alice = address(0xA11CE);
address bob = address(0xB0B);
address receiver = address(0xCAFE);
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
nft.mintProfile("Bob", 26, "ipfs://bob");
MultiSigWallet wallet = new MultiSigWallet(alice, bob);
vm.deal(address(wallet), 1 ether);
nft.blockProfile(alice);
assertEq(nft.profileToToken(alice), 0);
vm.prank(alice);
wallet.submitTransaction(receiver, 1 ether);
vm.prank(alice);
wallet.approveTransaction(0);
vm.prank(bob);
wallet.approveTransaction(0);
vm.prank(alice);
wallet.executeTransaction(0);
assertEq(receiver.balance, 1 ether);
}

Recommended Mitigation

If blocking is intended to revoke access to app-created multisigs, make the multisig aware of the profile NFT contract and require both owners to remain active profiles before allowing owner actions.

contract MultiSigWallet {
+ SoulboundProfileNFT public immutable profileNFT;
address public owner1;
address public owner2;
- constructor(address _owner1, address _owner2) {
+ constructor(address _owner1, address _owner2, address _profileNFT) {
require(_owner1 != address(0) && _owner2 != address(0), "Invalid owner address");
require(_owner1 != _owner2, "Owners must be different");
owner1 = _owner1;
owner2 = _owner2;
+ profileNFT = SoulboundProfileNFT(_profileNFT);
}
modifier onlyOwners() {
if (msg.sender != owner1 && msg.sender != owner2) revert NotAnOwner();
+ require(profileNFT.profileToToken(msg.sender) != 0, "Blocked profile");
_;
}
}

Alternatively, track blocked addresses in a shared registry and have multisigs consult that registry for authorization.

Updates

Lead Judging Commences

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