Summary
Per the README
, when there is a mutual likeness between two users, they are meant to get 90% of all their previous like payments pooled into a shared MultiSigWallet
, and the protocol gets a 10% fee.
But with current setup, none of these is realized; neither 90% funds, or 10% fees.
Vulnerability Details
The LikeRegistry::userBalances
is not updated with the payment of each caller of the LikeRegistry::likeUser()
function.
Impact
This means that there are no funds sent to the shared MultiSigWallet
, and the protocol realizes no fees.
PoC
I added the following getter function to the LikeRegistry
contract to enable us get the totalFees
the protocol realizes:
function getTotalFees() external view returns (uint256) {
return totalFees;
}
Here is the test that validates this finding:
pragma solidity ^0.8.19;
import {Test, console2} from "forge-std/Test.sol";
import {LikeRegistry} from "../src/LikeRegistry.sol";
import {SoulboundProfileNFT} from "../src/SoulboundProfileNFT.sol";
contract TestLikeRegistry is Test {
LikeRegistry registry;
SoulboundProfileNFT nft;
address bale = address(0x1);
address sarah = address(0x2);
address vendor = address(0x3);
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.prank(bale);
nft.mintProfile("Bale", 27, "ipfs://profileImage");
vm.prank(sarah);
nft.mintProfile("Sarah", 24, "ipfs://profileImage");
deal(bale, 1 ether);
deal(sarah, 1 ether);
}
function testNoFeesAsWellAsNoFundsInMultiSIgWalletForLovers() public {
vm.prank(bale);
registry.likeUser{value: 1 ether}(sarah);
vm.prank(sarah);
registry.likeUser{value: 1 ether}(bale);
uint256 totalFees = registry.getTotalFees();
assert(totalFees == 0);
vm.prank(nft.owner());
vm.expectRevert();
registry.withdrawFees();
}
receive() external payable {}
}
Run the testNoFeesAsWellAsNoFundsInMultiSIgWalletForLovers
test and with the -vvvv
flag, and read the traces to see that there are no funds sent to the newly created MultiSIgWallet
:
Traces:
[713486] TestLikeRegistry::testNoFeesAsWellAsNoFundsInMultiSIgWalletForLovers()
├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [37536] LikeRegistry::likeUser{value: 1000000000000000000}(SHA-256: [0x0000000000000000000000000000000000000002])
│ ├─ [2627] SoulboundProfileNFT::profileToToken(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [2627] SoulboundProfileNFT::profileToToken(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: ECRecover: [0x0000000000000000000000000000000000000001], liked: SHA-256: [0x0000000000000000000000000000000000000002])
│ └─ ← [Stop]
├─ [0] VM::prank(SHA-256: [0x0000000000000000000000000000000000000002])
│ └─ ← [Return]
├─ [639534] LikeRegistry::likeUser{value: 1000000000000000000}(ECRecover: [0x0000000000000000000000000000000000000001])
│ ├─ [627] SoulboundProfileNFT::profileToToken(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ [627] SoulboundProfileNFT::profileToToken(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ emit Liked(liker: SHA-256: [0x0000000000000000000000000000000000000002], liked: ECRecover: [0x0000000000000000000000000000000000000001])
│ ├─ emit Matched(user1: SHA-256: [0x0000000000000000000000000000000000000002], user2: ECRecover: [0x0000000000000000000000000000000000000001])
│ ├─ [483834] → new MultiSigWallet@0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
│ │ └─ ← [Return] 2193 bytes of code
│ ├─ [55] MultiSigWallet::receive() 👈🏾👈🏾👈🏾
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [291] LikeRegistry::getTotalFees() [staticcall]
│ └─ ← [Return] 0
├─ [2441] SoulboundProfileNFT::owner() [staticcall]
│ └─ ← [Return] TestLikeRegistry: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]
├─ [0] VM::prank(TestLikeRegistry: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [2619] LikeRegistry::withdrawFees()
│ └─ ← [Revert] revert: No fees to withdraw
└─ ← [Stop]
Tools Used
Recommendations
Update the userBalances
of each user after they like another user:
function likeUser(address liked) external payable {
...
+ userBalances[msg.sender] += msg.value;
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
...
}
Here is a test that confirms that this mitigation works:
function testNoFeesAsWellAsNoFundsInMultiSIgWalletForLovers() public {
vm.prank(bale);
registry.likeUser{value: 1 ether}(sarah);
vm.prank(sarah);
registry.likeUser{value: 1 ether}(bale);
uint256 totalFees = registry.getTotalFees();
assert(totalFees == 0.2 ether);
vm.prank(nft.owner());
registry.withdrawFees();
}