Ethernaut: Levels 1 to 3
April 15, 2020•962 words
The Ethernaut is a Web3/Solidity based wargame inspired from overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.
Level 1 - Fallback
Target: claim ownership of the contract & reduce its balance to 0.
Contract
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
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 {
owner.transfer(address(this).balance);
}
function() payable external {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
Weakness
The contract's fallback function can owneship of the contract. The conditional requirements are not secure: any contributor can become owner after sending any value to the contract.
Solidity concept: fallback function
A contract can have at most one fallback function, declared using fallback () external payable. This function cannot have arguments, cannot return anything and must have external visibility. It is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable.
Like any function, the fallback function can execute complex operations as long as there is enough gas passed on to it.
Hack
- Contribute
- Send any amount to the contract, which will trigger the fallback.
- Conditional requirements will be met
- Sender becomes the owner
Takeaways
- Be careful when implementing a fallback that changes state as it can be triggered by anyone sending ETH to the contract.
- Avoid writing a fallback that can perform critical actions such as changing ownership or transfer funds.
- A common pattern is to let the fallback only emit events (e.g emit FundsReceived).
Level 2 - Fallout
Target: claim ownership of the contract.
Contract
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
Weakness
The contract used a syntax deprecated since v 0.5. The function meant to be the constructor isn't one. It can actually be called after contract initialisation. It has a public visibility and can be called by anyone.
Solidity Concept: constructor
A constructor is an optional function declared with the constructor keyword which is executed upon contract creation, and where you can run contract initialisation code.
Before the constructor code is executed, state variables are initialised to their specified value if you initialise them inline, or zero if you do not.
Prior to version 0.4.22, constructors were defined as functions with the same name as the contract. This syntax was deprecated and is not allowed anymore in version 0.5.0.
The Fal1out()
function was supposed to be named Fallout()
and would have been the contract's constructor as syntax previous version 0.5.
Hack
Simply call the Fal1out() function
.
Takeaway
- Work with the lastest compiler versions which are more secure.
- Listen to the compiler warnings.
- Do test driven development to detect typos.
Level 3 - Coin Flip
Target: guess the correct outcome 10 times in a row.
Contract
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
Weakness
The contract tries to create randomness by relying on blockhashes, block number and a given FACTOR
. This data isn't secret:
blockhash()
andblock.number
are global variables in solidity- the
FACTOR
used to compute thecoinFlip
value can be reused by the attacker
Solidity Concepts
blockhash(uint blockNumber) returns (bytes32)
: hash of the given block - only works for 256 most recent blocks
block.number (uint)
: current block number
Hack
Deploy an attacker contract:
The attacker contract compute itself the blockValue
by using the block.number
and blockhash()
global variables.
As FACTOR
is known, the attacker contract can next computecoinFlip
and side
.
We pass the right side
argument to the original flip function that we call from the attacker contract.
Takeaways
There’s no true randomness on Ethereum blockchain, only "pseudo-randomness": random generators that are considered “good enough”.
There currently isn't a native way to generate them.
Everything used in smart contracts is publicly visible, including the local variables and state variables marked as private.
Miners also have control over things like blockhashes, timestamps, and whether to include certain transactions - which allows them to bias these values in their favor.