Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: low
Invalid

Inconsistent Treat Cost Handling for Pending Purchases in SpookySwap

01. Relevant GitHub Links

02. Summary

The SpookySwap contract allows adding treats with the same name but different costs, which causes issues with pending treat purchases. This inconsistency in treat costs can lead to transaction reverts and disrupt user experience.

03. Vulnerability Details

The SpookySwap contract has a vulnerability where treats with the same name can be re-added with a different cost while there are pending purchases for that treat. As a result, users who have already committed to purchasing a treat with an initial price might face unexpected reverts if the treat’s cost is later modified, even if they provided sufficient ETH for the original cost.

This creates an inconsistency in the treat cost for pending purchases and affects the functionality of resolveTrick, potentially leading to failed transactions.

03. Impact

  • Transaction Reverts: Pending purchases might revert if the treat’s cost is updated after the initial purchase but before resolveTrick is called, causing the user to lose funds due to unexpected reverts.

  • Disrupted User Experience: Users may experience failed transactions even though they initially provided the correct amount of ETH, leading to frustration and confusion.

  • Possible Exploitation: Malicious actors could potentially exploit this inconsistency by manipulating treat costs to interfere with user transactions.

04. Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {SpookySwap} from "../src/TrickOrTreat.sol";
contract TrickOrTreatTest is Test {
SpookySwap spookyswapInstance;
address owner;
address user;
function setUp() public {
owner = vm.addr(0x1);
user = vm.addr(0x2);
vm.label(owner, "Owner");
vm.deal(owner, 100 ether);
vm.label(user, "User");
vm.deal(user, 100 ether);
SpookySwap.Treat[] memory TreatList;
vm.prank(owner);
spookyswapInstance = new SpookySwap(TreatList);
}
function test_poc_addTreatPendingTreatChaneTreatCost() public {
vm.prank(owner);
spookyswapInstance.addTreat("Example", 1 ether, "test");
uint256 tokenId = spookyswapInstance.nextTokenId();
// random = 2
// double price
vm.warp(123456790);
vm.prevrandao(987654324);
vm.prank(user);
spookyswapInstance.trickOrTreat{value: 1 ether}("Example");
// check cost of Pending Token
uint256 cost;
(, cost, ) = spookyswapInstance.treatList(spookyswapInstance.tokenIdToTreatName(tokenId));
console.log("Before Pending Token Cost: ", cost);
// addTreat same name with Pending Token
vm.prank(owner);
spookyswapInstance.addTreat("Example", 5 ether, "test");
// check cost of Pending Token
(, cost, ) = spookyswapInstance.treatList(spookyswapInstance.tokenIdToTreatName(tokenId));
console.log("Before Pending Token Cost: ", cost);
// Revert due to price change of pending token
vm.prank(user);
vm.expectRevert("Insufficient ETH sent to complete purchase");
spookyswapInstance.resolveTrick{value: 2 ether}(tokenId);
}
}
$ forge test --mt test_poc_addTreatPendingTreatChaneTreatCost -vvvv
[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.27
[⠆] Solc 0.8.27 finished in 2.19s
Compiler run successful!
Ran 1 test for test/test.t.sol:TrickOrTreatTest
[PASS] test_poc_addTreatPendingTreatChaneTreatCost() (gas: 355496)
Logs:
random 2
Before Pending Token Cost: 1000000000000000000
Before Pending Token Cost: 5000000000000000000
Traces:
[358296] TrickOrTreatTest::test_poc_addTreatPendingTreatChaneTreatCost()
├─ [0] VM::prank(Owner: [0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf])
│ └─ ← [Return]
├─ [118652] SpookySwap::addTreat("Example", 1000000000000000000 [1e18], "test")
│ ├─ emit TreatAdded(name: "Example", cost: 1000000000000000000 [1e18], metadataURI: "test")
│ └─ ← [Stop]
├─ [2341] SpookySwap::nextTokenId() [staticcall]
│ └─ ← [Return] 1
├─ [0] VM::warp(123456790 [1.234e8])
│ └─ ← [Return]
├─ [0] VM::prevrandao(987654324 [9.876e8])
│ └─ ← [Return]
├─ [0] VM::prank(User: [0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF])
│ └─ ← [Return]
├─ [155065] SpookySwap::trickOrTreat{value: 1000000000000000000}("Example")
│ ├─ [0] console::log("random", 2) [staticcall]
│ │ └─ ← [Stop]
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SpookySwap: [0xF2E246BB76DF876Cef8b38ae84130F4F55De395b], tokenId: 1)
│ ├─ emit MetadataUpdate(_tokenId: 1)
│ ├─ emit Swapped(user: User: [0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF], treatName: "Example", tokenId: 1)
│ └─ ← [Stop]
├─ [1464] SpookySwap::tokenIdToTreatName(1) [staticcall]
│ └─ ← [Return] "Example"
├─ [3042] SpookySwap::treatList("Example") [staticcall]
│ └─ ← [Return] "Example", 1000000000000000000 [1e18], "test"
├─ [0] console::log("Before Pending Token Cost: ", 1000000000000000000 [1e18]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::prank(Owner: [0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf])
│ └─ ← [Return]
├─ [28952] SpookySwap::addTreat("Example", 5000000000000000000 [5e18], "test")
│ ├─ emit TreatAdded(name: "Example", cost: 5000000000000000000 [5e18], metadataURI: "test")
│ └─ ← [Stop]
├─ [1464] SpookySwap::tokenIdToTreatName(1) [staticcall]
│ └─ ← [Return] "Example"
├─ [3042] SpookySwap::treatList("Example") [staticcall]
│ └─ ← [Return] "Example", 5000000000000000000 [5e18], "test"
├─ [0] console::log("Before Pending Token Cost: ", 5000000000000000000 [5e18]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::prank(User: [0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF])
│ └─ ← [Return]
├─ [0] VM::expectRevert(Insufficient ETH sent to complete purchase)
│ └─ ← [Return]
├─ [6824] SpookySwap::resolveTrick{value: 2000000000000000000}(1)
│ └─ ← [Revert] revert: Insufficient ETH sent to complete purchase
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.34ms (1.56ms CPU time)
Ran 1 test suite in 792.45ms (11.34ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

05. Tools Used

Manual Code Review and Foundry

06. Recommended Mitigation

  1. Ensure Unique Treat Names: Treat names should be unique in the system to avoid cost inconsistency issues. Adding a require statement in the addTreat function can enforce this.

require(treats[treatName].cost == 0, "Treat with this name already exists.");
  1. Lock Treat Cost for Pending Purchases: Prevent the cost of a treat from being updated if there are pending purchases that depend on the original cost. Implementing a check in addTreat to verify no pending transactions can help maintain consistent costs.

Updates

Appeal created

bube Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[invalid] Duplicate treats

The function `addTreat` is called by the owner. The owner is trusted. There will be no duplicates.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.