Summary
The contract never updates userBalances
after receiving the ETH, meaning the funds are never properly tracked or transferred. Instead, the ETH just sits in the contract with no way for users to claim it.
mapping(address => uint256) public userBalances;
Vulnerability Details
The matchRewards
function reads the userBalances
to calculate the starting funds of the multisig. But because the mapping is never updated, the rewards
variable used in .call
will always be zero.
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success, ) = payable(address(multiSigWallet)).call{
value: rewards
}("");
require(success, "Transfer failed");
}
Impact
User funds are permanently locked, because there is no other method to withdraw them from the contract. The MultiSig wallets receive 0 ETH, making them useless.
Proof of Code: Copy this code in a new .t.sol file. The main test tracks the funds movements in the console. It shows that the MultiSig::executeTransaction
function reverts due to out-of-funds.
WARNING: LikeRegistry::matchRewards
doesn't emit an event when creating the multisig. You need to check in the terminal the address of the multisig created and set it to the multiSig
variable in case it creates a different address.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/MultiSig.sol";
import "../src/LikeRegistry.sol";
contract ProofOfCode is Test {
SoulboundProfileNFT soulboundNFT;
MultiSigWallet multiSigWallet;
LikeRegistry likeRegistry;
address user = address(0x123);
address user2 = address(0x456);
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
vm.deal(user, 1 ether);
vm.deal(user2, 1 ether);
}
function testMultiSigFails() public {
console.log(
"Balance of LikeRegistry BEFORE calling likeUser: %s",
address(likeRegistry).balance
);
console.log(
"Balance of User1 BEFORE calling likeUser: %s",
address(user).balance
);
console.log(
"Balance of User2 BEFORE calling likeUser: %s",
address(user2).balance
);
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.startPrank(user2);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
likeRegistry.likeUser{value: 1 ether}(user);
vm.stopPrank();
vm.startPrank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
address payable multiSig = payable(
0xffD4505B3452Dc22f8473616d50503bA9E1710Ac
);
console.log(
"Starting balance of Mulisig: %s",
address(multiSig).balance
);
console.log(
"Balance of LikeRegistry AFTER calling likeUser: %s",
address(likeRegistry).balance
);
console.log(
"Balance of User1 AFTER calling likeUser: %s",
address(user).balance
);
console.log(
"Balance of User2 AFTER calling likeUser: %s",
address(user2).balance
);
MultiSigWallet(multiSig).submitTransaction(
address(likeRegistry),
1 ether
);
MultiSigWallet(multiSig).approveTransaction(0);
vm.stopPrank();
vm.startPrank(user2);
MultiSigWallet(multiSig).approveTransaction(0);
vm.expectRevert();
MultiSigWallet(multiSig).executeTransaction(0);
vm.stopPrank();
}
}
Tools Used
Foundry - Slither
Recommendations
The developers must update userBalances
in likeUser()
. This ensures that users' funds are properly tracked and later transferred when a match happens:
function likeUser(address liked) external payable {
// HERE ARE THE CHECKS
likes[msg.sender][liked] = true;
+ userBalances[msg.sender] += msg.value;
emit Liked(msg.sender, liked);
// FUNCTION CONTINUES
}