trickOrTreat
FunctionThe trickOrTreat
function in the provided Solidity contract is vulnerable to a rollback attack. An attacker can repeatedly call the function and revert the transaction when the result is not favorable (e.g., no half-price purchase), allowing them to bypass the intended randomness and eventually purchase NFTs at a lower cost.
The vulnerability exists in the random number generation and logic in the trickOrTreat
function:
Predictability of Random Number: The random number is generated using on-chain variables like block.timestamp
, msg.sender
, nextTokenId
, and block.prevrandao
. These variables can be partially predicted or manipulated by the attacker.
Transaction Revert Mechanism: An attacker can call trickOrTreat
and, based on the result, decide whether to revert the transaction. If the random result is unfavorable (e.g., not a half-price purchase), the attacker can revert to avoid paying unnecessary costs.
Repeated Attempts: The attacker can call the function multiple times, reverting unfavorable outcomes, until they achieve a desirable result.
Retrieve treat.cost
: The attacker first retrieves the cost of the specific Treat
by calling the target contract’s treatList
function.
Call trickOrTreat
: The attacker sends enough ETH (at least to cover the possible maximum requiredCost
) and calls trickOrTreat
.
Evaluate the Result:
The attacker computes the actual cost they paid based on their balance before and after the transaction.
They calculate the possible requiredCost
values (half price, normal price, or double price) based on the contract logic.
They compare the actual cost to the possible requiredCost
values to determine the result.
Revert if Unfavorable:
If the result is a half-price purchase (random == 1
), the transaction succeeds, and the attacker benefits.
If the result is unfavorable, the attacker reverts the transaction to avoid paying the full or double price.
Repeat the Attack: The attacker repeats the process until they achieve the desired half-price result.
Financial Loss: The attacker can purchase NFTs at a significantly lower cost than intended, leading to financial losses for the contract owner or other users.
Unfair Advantage: The attacker can manipulate the random outcome to gain an unfair advantage over other users.
Increased Network Load: The repeated transactions and rollbacks increase the load on the blockchain network, affecting the experience for other users.
To prevent the exploitation of the vulnerability, one possible mitigation strategy is to avoid accepting contract calls. Since contracts can include fallback functions to automatically react to received funds, it is important to ensure that only externally owned accounts (EOAs) are allowed to interact with the trickOrTreat
function.
To enforce this, you can add a modifier that ensures only EOAs can call certain functions:
The following contract demonstrates how an attacker can exploit the rollback attack to repeatedly attempt to purchase NFTs at half price.
Deploy the Target Contract:
Deploy the provided SpookySwap
contract on a local testnet or public testnet.
Ensure that a Treat
named "Candy"
is available with a set cost
.
Deploy the Attack Contract:
Deploy the RollbackAttack
contract on the same network.
Pass the address of the target contract (SpookySwap
) into the constructor of the attack contract.
Execute the Attack:
Call the attack()
function on the RollbackAttack
contract.
Ensure to send enough ETH to cover the maximum potential cost (at least the double price of the treat).
The RollbackAttack
contract will attempt to exploit the trickOrTreat
function by repeatedly calling it and reverting the transaction if the outcome is unfavorable.
Verify the Results:
Check the successCount
in the RollbackAttack
contract. If it increases, this indicates a successful half-price purchase.
Verify that the attacker's address has received the NFT from the target contract by checking the NFT ownership.
Use Secure Randomness:
Implement an off-chain randomness oracle (such as Chainlink VRF) to generate unpredictable and verifiable random numbers. This would prevent attackers from manipulating outcomes through retries.
Increase Transaction Costs:
Introduce non-refundable fees in the trickOrTreat
function, so attackers face higher costs for repeatedly attempting to manipulate the random number.
Limit Retries:
Limit the number of times a user can call the trickOrTreat
function in a given time frame, preventing them from making unlimited attempts.
State-Changing Operations:
Introduce irreversible state changes earlier in the function execution to ensure that attackers face consequences even when reverting the transaction.
Improve Transaction Logic:
Modify the logic so that part of the user's payment is consumed as soon as the transaction starts or after random number generation, making it costly to revert and retry.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.