An auction contract has a critical vulnerability that allows an attacker to exploit the auction process. This flaw enables the attacker to place bids even after the auction has ended and repeatedly call the claimTokens
function, which can drain the auction's funds. The vulnerability threatens the integrity and fairness of the auction, potentially causing significant financial loss.
The issue stems from the misuse of block.timestamp
, which is a common time-based function in smart contracts. block.timestamp
can be manipulated by miners to some extent, which introduces a risk that the auction's timing logic can be exploited.
Here’s how the attack unfolds:
Token Approval: The attacker authorizes the auction contract to spend a certain amount of fjordPoints
tokens (1 ether * number of users entering the auction). This sets up the attack by giving the auction contract control over the attacker’s tokens.
Initial Bid: The attacker places an initial bid of 1 ether, participating in the auction as any other user would.
Auction End: The attacker waits until the block.timestamp
exactly matches the auction's end time (auctionEndTime) plus 1 second. They then call the auctionEnd
function to finalize the auction.
Token Claiming: After the auction ends, the attacker starts calling the claimTokens
function to claim the auctioned tokens. They can do this repeatedly, allowing them to claim the same tokens multiple times.
Re-bidding and Claiming: The attacker can place another bid (e.g., 1 ether) and call claimTokens
again, exploiting the contract to claim more tokens than they should be able to, based on the total number of users in the auction.
This allows the attacker to drain the auction's funds, as they can effectively claim the auction tokens multiple times, leaving less or no tokens for legitimate participants.
Bid Function: The contract prevents users from bidding if block.timestamp
exceeds auctionEndTime
, which is supposed to stop bidding after the auction ends.
Auction End Function: Similarly, it prevents the auction from ending if block.timestamp
is less than auctionEndTime
, ensuring the auction runs its full duration.
However, the logic overlooks the scenario where block.timestamp
equals auctionEndTime
. At this precise moment, an attacker can exploit the timing to drain the auction's tokens and prevent other users from claiming their rightful tokens after the auction has ended.
Add this Test Function to auction.t.sol
This PoC demonstrates how an attacker can exploit a vulnerability in the auction contract to repeatedly drain auction funds by bidding after the auction has ended and then repeatedly claiming tokens.
Setup:
The test creates four users (user0
, user1
, user2
, user3
), each represented by an Ethereum address.
deal()
function is used to allocate fjordPoints
tokens to each user:
user0
receives 5 ETH worth of fjordPoints
.
user1
, user2
, and user3
each receive 1 ETH worth of fjordPoints
.
Initial Bids:
Each user starts with a specific amount of tokens (fjordPoints
), and they approve the auction contract to spend these tokens on their behalf.
User 0: Approves the auction contract to spend 1 ether + 4 ether
worth of tokens and places an initial bid of 1 ether.
User 1, 2, and 3: Each approves the auction contract to spend 1 ether and then places a bid of 1 ether.
After all bids:
The total number of bids is 4 FJO
.
The auction contract holds 1000 AUCT
tokens.
Auction End:
The test simulates the passage of time using vm.warp(biddingTime)
and skip(1)
, setting the block timestamp to one second after the auction end time.
The auction.auctionEnd()
function is called to finalize the auction.
The multiplier is calculated based on the final bids.
Attack Execution:
User 0 then exploits the vulnerability:
First Claim: Calls claimTokens()
and successfully claims AUCT
tokens based on their bid.
Re-bid and Re-claim: After claiming, user0
places a new bid of 1 ether and then immediately calls claimTokens()
again. This process is repeated multiple times.
Each time user0
bids and claims, they receive more AUCT
tokens, even though the auction has technically ended.
Outcome:
By the end of the attack, user0
has claimed all AUCT
tokens in the auction contract.
The final console log shows that the auction contract's balance of AUCT
tokens is reduced to 0, meaning the auction's funds have been completely drained.
The vulnerability in the auction contract arises due to the dangerous usage of block.timestamp
, which can be manipulated by miners. Here's how a miner could exploit this:
Auction Timing Logic:
The auction contract uses block.timestamp
to determine when the auction starts, ends, and when bids can be placed or tokens can be claimed.
The key issue here is that the contract relies on block.timestamp
comparisons like block.timestamp > auctionEndTime
in the bid
function and block.timestamp < auctionEndTime
in the auctionEnd
function.
Edge Case:
If block.timestamp
exactly equals auctionEndTime
, the contract's logic might not properly handle bids or end the auction correctly, potentially allowing unintended behaviors.
Miners have some control over the block timestamp within a small range, which they can use to their advantage. Here's how:
Manipulating block.timestamp
:
Miners can set the block timestamp to a value that is advantageous for them, within the allowable range of the current network time (usually within 15 seconds in the past or future).
In this case, a miner could set block.timestamp
to be exactly equal to auctionEndTime
.
Exploiting the Auction Logic:
Scenario 1: Bidding After Auction End:
If the miner is also a participant in the auction (or colluding with one), they can mine a block where block.timestamp == auctionEndTime
.
If the bid
function does not properly handle this exact condition, the miner could submit a bid after the auction should have ended. This could allow them to bid again and potentially win the auction or manipulate the bidding process unfairly.
Scenario 2: Claiming Tokens Multiple Times:
After setting block.timestamp == auctionEndTime
, the miner can call auctionEnd
to finalize the auction.
They could then exploit the logic to call claimTokens
multiple times, as the contract might not properly prevent re-bidding and claiming due to the flawed timestamp logic.
This would allow the miner to drain the auction's funds by repeatedly claiming more tokens than they are entitled to.
Financial Loss: The miner could drain the auction contract's funds, leading to significant financial loss for other participants.
Unfair Auction Process: The integrity and fairness of the auction process are compromised, as the miner can manipulate the outcome by controlling when the auction ends and how tokens are distributed.
Strict Auction Time Checks:
Ensure Proper Time Comparisons:
Modify the bid
and auctionEnd
functions to use block.timestamp >= auctionEndTime
for preventing any bids after the auction end time and block.timestamp > auctionEndTime
for ending the auction.
Avoid relying on block.timestamp == auctionEndTime
to avoid edge cases where an attacker could exploit a precise block timestamp.
Finalize Auction Before Token Claims:
Require Auction Finalization:
Ensure that the claimTokens
function can only be called after the auction has been fully finalized. This would involve adding a check that the auction has been ended (auctionEnded
flag or similar) and that no further bids are allowed before any tokens are claimed.
Example:
Prevent Multiple Claims:
Limit claimTokens
to One Call Per User:
Add a mechanism to track whether a user has already claimed their tokens to prevent multiple claims. This could be a mapping that stores a boolean flag for each user who has claimed their tokens.
Example:
Additional Safeguards:
Review All Time-Dependent Logic:
Ensure that other functions dependent on block.timestamp
are robust against edge cases or miner manipulation.
Consider using a block number-based approach for timing to reduce the risk of block.timestamp
manipulation.
The protocol doesn't properly treat the `block.timestamp == auctionEndTime` case. Impact: High - There are at least two possible impacts here: 1. By chance, user bids could land in a block after the `auctionEnd()` is called, not including them in the multiplier calculation, leading to a situation where there are insufficient funds to pay everyone's claim; 2. By malice, where someone can use a script to call `auctionEnd()` + `bid(totalBids)` + `claimTokens()`, effectively depriving all good faith bidders from tokens. Likelihood: Low – The chances of getting a `block.timestamp == auctionEndTime` are pretty slim, but it’s definitely possible.
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.