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");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
modifier onlyOwners() {
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:
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.