DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

Missing checks in `MultiSigWallet::submitTransaction` function could allow either of the two owners to drain funds from the contract

Description

The MultiSigWallet::submitTransaction function doesn't have any check for the maximum amount (_value) that an owner can withdraw from the contract, and also there is no limit on the number of transactions that an owner can submit. Although, the transaction can't be executed until the MultiSigWallet::approveTransaction function is being called by both the owners, so the owner can optionally decide not to approve the transaction submitted by the other owner, but it would still impose the problem of either a lot of unapproved transactions in the queue (i.e., transactions array) or the owner accidentally approving the transaction without inspecting the values of the underlying fields manually.

Impact

Either of the two owners can drain the MultiSigWallet contract, causing the other owner losing their funds.

Proof of Concept

  1. Once a match is being made and the MultiSigWallet contract is deployed and funded with ether.

  2. Either of the two owners (i.e., users corresponding to the match) of the MultiSigWallet contract can call MultiSigWallet::submitTransaction function any number of times and by passing any _value greater than zero.

  3. If the other owner accidentally calls the MultiSigWallet::approveTransaction function without inspecting the values of the underlying fields manually, then it could result in the draining of funds from the MultiSigWallet contract.

Proof of Code

Since there isn't a way to track MultiSigWallet contract address corresponding to the match, therefore I've added the return statements in the LikeRegistry::likeUser and LikeRegistry::matchRewards functions, basically a kind of mutation testing.

So, the revised LikeRegistry::likeUser and LikeRegistry::matchRewards functions would be:

function likeUser(address liked) external payable returns(address matchToMultiSigWallet) {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
require(!likes[msg.sender][liked], "Already liked");
require(msg.sender != liked, "Cannot like yourself");
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
likes[msg.sender][liked] = true;
userBalances[liked] += 1 ether; // Recommended mitigation from the other HIGH severity vulnerability that I submitted
emit Liked(msg.sender, liked);
// Check if mutual like
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchToMultiSigWallet = matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal returns(address) {
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;
// Deploy a MultiSig contract for the matched users
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// Send ETH to the deployed multisig wallet
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
return address(multiSigWallet);
}

Then, create a test file named as DatingDappTest.t.sol inside the test folder with the following content:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test} from "forge-std/Test.sol";
import {LikeRegistry} from "../src/LikeRegistry.sol";
import {SoulboundProfileNFT} from "../src/SoulboundProfileNFT.sol";
import {MultiSigWallet} from "../src/MultiSig.sol";
contract DatingDappTest is Test {
SoulboundProfileNFT soulboundNFT;
LikeRegistry likeRegistry;
address owner = makeAddr("owner");
address bob = makeAddr("bob");
address alice = makeAddr("alice");
function setUp() public {
vm.startPrank(owner);
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
vm.stopPrank();
}
function testEitherOwnerCanDrainMultiSig() public {
vm.prank(bob);
soulboundNFT.mintProfile("Bob", 25, "ipfs://profileImage");
vm.prank(alice);
soulboundNFT.mintProfile("Alice", 24, "ipfs://profileImage");
hoax(bob, 1 ether);
address multiSigForMatch = likeRegistry.likeUser{value: 1 ether}(alice);
assertEq(multiSigForMatch, address(0), "There is no match because it's only one-sided like till now");
hoax(alice, 1 ether);
multiSigForMatch = likeRegistry.likeUser{value: 1 ether}(bob);
assertNotEq(multiSigForMatch, address(0), "There is a match because it's a mutual like now");
uint256 totalAmountInMultiSigWallet = multiSigForMatch.balance;
// The MultiSigWallet contract balance should be 1.8 ether (1 + 1 - 0.2)
assertEq(totalAmountInMultiSigWallet, 1.8 ether);
MultiSigWallet multiSigWallet = MultiSigWallet(payable(multiSigForMatch));
vm.startPrank(bob);
multiSigWallet.submitTransaction(bob, totalAmountInMultiSigWallet);
multiSigWallet.approveTransaction(0);
vm.stopPrank();
vm.prank(alice);
multiSigWallet.approveTransaction(0);
vm.prank(bob);
multiSigWallet.executeTransaction(0);
// Bob has drained the MultiSigWallet contract
assertEq(multiSigForMatch.balance, 0);
assertEq(bob.balance, totalAmountInMultiSigWallet);
}
}

Recommended Mitigation

Add the following checks in the MultiSigWallet::submitTransaction function:

+ error ValueExceedsTheMaxAllowedAmount(uint256 maxAllowedAmount);
+ error Owner1HasAlreadySubmittedTransaction();
+ error Owner2HasAlreadySubmittedTransaction();
/// @notice Submit a transaction for approval
function submitTransaction(address _to, uint256 _value) external onlyOwners {
if (_to == address(0)) revert InvalidRecipient();
if (_value == 0) revert InvalidAmount();
+ if (_value > address(this).balance / 2) revert ValueExceedsTheMaxAllowedAmount(address(this).balance / 2);
+ if (submittedTransactionByOwner1) revert Owner1HasAlreadySubmittedTransaction();
+ if (submittedTransactionByOwner2) revert Owner2HasAlreadySubmittedTransaction();
+ if (msg.sender == owner1) {
+ submittedTransactionByOwner1 = true;
+ } else {
+ submittedTransactionByOwner2 = true;
+ }
transactions.push(Transaction(_to, _value, false, false, false));
uint256 txId = transactions.length - 1;
emit TransactionCreated(txId, _to, _value);
}
Updates

Appeal created

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

Users mistake, only impacting themselves.

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.