DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: medium
Valid

Exploitable Flaw in Auction Contract Allows Repeated Bids and Token Draining After Auction Ends

Summary

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.

Vulnerability Details

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:

  1. 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.

  2. Initial Bid: The attacker places an initial bid of 1 ether, participating in the auction as any other user would.

  3. 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.

  4. 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.

  5. 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.

if (block.timestamp > auctionEndTime) { // in bid Function
revert AuctionAlreadyEnded();
}
if (block.timestamp < auctionEndTime) { // in auctionEnd Function
revert AuctionNotYetEnded();
}
  • 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.

PoC

Add this Test Function to auction.t.sol

function testAttackerCanBidAgainAfterAndTheifFunds_PrgZr0() public{
address user0 = address(0x01);
address user1 = address(0x02);
address user2 = address(0x03);
address user3 = address(0x04);
uint256 bidAmount = 1 ether;
deal(address(fjordPoints), user0, 5 ether);
deal(address(fjordPoints), user1, bidAmount);
deal(address(fjordPoints), user2, bidAmount);
deal(address(fjordPoints), user3, bidAmount);
vm.startPrank(user0);
fjordPoints.approve(address(auction), bidAmount + 4 ether);
auction.bid(bidAmount);
vm.stopPrank();
vm.startPrank(user1);
fjordPoints.approve(address(auction), bidAmount);
auction.bid(bidAmount);
vm.stopPrank();
vm.startPrank(user2);
fjordPoints.approve(address(auction), bidAmount);
auction.bid(bidAmount);
vm.stopPrank();
vm.startPrank(user3);
fjordPoints.approve(address(auction), bidAmount);
auction.bid(bidAmount);
vm.stopPrank();
console.log("Total Bids : ",auction.totalBids() / 1e18 , "FJO"); // 4 FJO
console.log("totalTokens : ",auctionToken.balanceOf(address(auction)) / 1e18 , "AUCT"); // 1000 AUCT
vm.warp(biddingTime); // biddingTime = 7 days = 604800
skip(1); // block.timestamp = 604801
auction.auctionEnd();
console.log(auction.multiplier());
// Attack
vm.startPrank(user0);
auction.claimTokens();
console.log("User0 claim 1 : ",auctionToken.balanceOf(user0) / 1e18 , "AUCT");
auction.bid(1 ether);
auction.claimTokens();
console.log("User0 claim 2 : ",auctionToken.balanceOf(user0) / 1e18 , "AUCT");
auction.bid(1 ether);
auction.claimTokens();
console.log("User0 claim 3 : ",auctionToken.balanceOf(user0) / 1e18 , "AUCT");
auction.bid(1 ether);
auction.claimTokens();
console.log("User0 claim 4 : ",auctionToken.balanceOf(user0) / 1e18, "AUCT");
vm.stopPrank();
console.log("After Attack totalTokens : ",auctionToken.balanceOf(address(auction)) / 1e18 , "AUCT"); // 0 AUCT
}
//Logs:
// Total Bids : 4 FJO
// totalTokens : 1000 AUCT
// User0 claim 1 : 250 AUCT
// User0 claim 2 : 500 AUCT
// User0 claim 3 : 750 AUCT
// User0 claim 4 : 1000 AUCT
// After Attack totalTokens : 0 AUCT

Explanation of the Proof of Concept (PoC)

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.

Step-by-Step Breakdown

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Understanding the Vulnerability

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:

  1. 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.

  2. 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.

How a Miner Can Exploit This:

Miners have some control over the block timestamp within a small range, which they can use to their advantage. Here's how:

  1. 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.

  2. 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.

Impact:

  • 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.

mitigation :

  1. 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.

  1. 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:

      require(auctionEnded, "Auction not yet finalized");
  1. 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:

      mapping(address => bool) public tokensClaimed;
      function claimTokens() external {
      require(!tokensClaimed[msg.sender], "Tokens already claimed");
      tokensClaimed[msg.sender] = true;
      // Proceed with token transfer
      }
  1. 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.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Users can bid in the same block when the actionEnd could be called (`block.timestamp==actionEndTime`), depending on the order of txs in block they could lose funds

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.

Support

FAQs

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