Summary
The contract does not properly handle return values from ETH transfers, making it vulnerable to false positive returns that could lead to inconsistent state.
Vulnerability Details
The contract assumes that a successful return value from a low-level call means the ETH transfer succeeded, but this can be manipulated:
A malicious contract can revert with a message that looks like true
The contract doesn't verify the actual balance changes
This can lead to state inconsistencies
function matchRewards(address from, address to) internal {
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}
Impact
State Inconsistency : Contract state could be updated even when ETH transfer actually failed
False Success Reports : External integrations could be misled about transfer success
Potential for Manipulation : Malicious contracts could exploit this behavior
Severity: MEDIUM - Can lead to state inconsistencies but requires specific conditions
Tools Used
Proof of Concept
contract FalsePositiveMultiSig {
address public owner1;
address public owner2;
constructor(address _owner1, address _owner2) {
owner1 = _owner1;
owner2 = _owner2;
}
receive() external payable {
revert("0x0000000000000000000000000000000000000000000000000000000000000001");
}
}
function testFalsePositiveTransfer() public {
console.log("Starting false positive transfer test");
vm.deal(alice, 2 ether);
vm.deal(bob, 2 ether);
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
FalsePositiveMultiSig falsePositiveMultiSig = new FalsePositiveMultiSig(alice, bob);
uint256 initialBalance = address(falsePositiveMultiSig).balance;
vm.expectRevert("0x0000000000000000000000000000000000000000000000000000000000000001");
(bool success,) = address(falsePositiveMultiSig).call{value: 1 ether}("");
assertEq(
address(falsePositiveMultiSig).balance,
initialBalance,
"Contract should not have received ETH"
);
}
Recommendations
Verify actual balance changes after transfers:
function matchRewards(address from, address to) internal {
uint256 initialBalance = address(multiSigWallet).balance;
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
require(
address(multiSigWallet).balance == initialBalance + rewards,
"ETH transfer verification failed"
);
}
Use OpenZeppelin's Address.sendValue()
:
using Address for address payable;
function matchRewards(address from, address to) internal {
payable(address(multiSigWallet)).sendValue(rewards);
}
Implement a try-catch pattern for transfers:
function matchRewards(address from, address to) internal {
uint256 initialBalance = address(multiSigWallet).balance;
try payable(address(multiSigWallet)).call{value: rewards}("") returns (bool success) {
require(success, "Transfer failed");
require(
address(multiSigWallet).balance == initialBalance + rewards,
"ETH not received"
);
} catch (bytes memory reason) {
revert(string(abi.encodePacked("Transfer failed: ", reason)));
}
}