DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Critical Flaw in FjordAuction: Users Risk Total Loss of Bids When USDC is Used as Auction Token

Summary

The FjordAuction smart contract contains a critical vulnerability related to precision handling when using USDC (that has 6 decimal places) as the auction token. The contract uses a fixed precision of 1e18 for calculations, which is inappropriate for tokens like USDC with 6 decimal places. This mismatch can result in users with smaller bids receiving no tokens due to precision loss, effectively losing their bids without compensation.

Vulnerability Details

https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L181

function auctionEnd() external {
if (block.timestamp < auctionEndTime) {
revert AuctionNotYetEnded();
}
if (ended) {
revert AuctionEndAlreadyCalled();
}
ended = true;
emit AuctionEnded(totalBids, totalTokens);
if (totalBids == 0) {
auctionToken.transfer(owner, totalTokens);
return;
}
multiplier = totalTokens.mul(PRECISION_18).div(totalBids);//@audit
// Burn the FjordPoints held by the contract
uint256 pointsToBurn = fjordPoints.balanceOf(address(this));
fjordPoints.burn(pointsToBurn);
}

The vulnerability stems from the use of a fixed PRECISION_18 constant (1e18) in the contract, which is suitable for 18 decimal place tokens like Ether but not for 6 decimal place tokens ,USDC. This issue manifests in the auctionEnd() and claimTokens() functions:

In auctionEnd():

multiplier = totalTokens.mul(PRECISION_18).div(totalBids);

  • In claimTokens():

    uint256 claimable = userBids.mul(multiplier).div(PRECISION_18);

Scenario:

Imagine we have an auction for 1,000,000 USDC tokens (6 decimal places). Two bidders participate:

  1. Bidder1: Places a small bid of 0.00002 FjordPoints (equivalent to 2 * 10^13 wei)

  2. Bidder2: Places a larger bid of 2 FjordPoints

Let's calculate this:

  • totalTokens = 1,000,000 * 10^6 (USDC has 6 decimal places)

  • totalBids = 2.00002 * 10^18

  • PRECISION_18 = 10^18

multiplier = (1,000,000 * 10^6 * 10^18) / (2.00002 * 10^18) ≈ 499,997,500,012,499,937.

For Bidder1: claimable = (0.00002 * 10^18 * 499,997,500,012,499,937) / 10^18 ≈ 9,999,950,000 This rounds down to 0 when converted to USDC's 6 decimal places.

For Bidder2: claimable = (2 * 10^18 * 499,997,500,012,499,937) / 10^18 ≈ 999,995,000,024,999,874 This rounds to 999,995 USDC.

Bidder1, despite participating in the auction and spending FjordPoints, receives absolutely nothing in return due to the precision mismatch between the contract's calculations (using 18 decimal places) and USDC's 6 decimal places. This unfairly penalizes smaller bidders and can lead to a loss of funds for these participants.

PoC

function testPrecisionLossInClaimTokens() public {
address bidder1 = address(0x2);
address bidder2 = address(0x3);
uint256 bidAmount1 = 2 * 10**13; // 0.00002 Ether in FjordPoints
uint256 bidAmount2 = 2 * 10**18; // 2 ether in FjordPoints
// Mint FjordPoints to the bidders
deal(address(fjordPoints), bidder1, bidAmount1);
deal(address(fjordPoints), bidder2, bidAmount2);
// Start bidding
vm.startPrank(bidder1);
fjordPoints.approve(address(auction), bidAmount1);
auction.bid(bidAmount1);
vm.stopPrank();
vm.startPrank(bidder2);
fjordPoints.approve(address(auction), bidAmount2);
auction.bid(bidAmount2);
vm.stopPrank();
// Skip to the end of the auction
skip(biddingTime);
auction.auctionEnd();
// Claim tokens
vm.prank(bidder1);
auction.claimTokens();
vm.prank(bidder2);
auction.claimTokens();
// Check the actual claimed amounts
uint256 bidder1ClaimedAmount = auctionToken.balanceOf(bidder1);
uint256 bidder2ClaimedAmount = auctionToken.balanceOf(bidder2);
// Assert that bidder1 receives 0 USDC due to precision loss
assertEq(bidder1ClaimedAmount, 0, "Bidder 1 should receive 0 USDC due to precision loss");
assert(bidder2ClaimedAmount>0);
}

run

forge test --match-test testPrecisionLossInClaimTokens -vvv

Result:

forge test --match-test testPrecisionLossInClaimTokens -vvv
[⠒] Compiling...
[⠆] Compiling 1 files with Solc 0.8.21
[⠰] Solc 0.8.21 finished in 3.16s
Compiler run successful!
Ran 1 test for test/unit/auction.t.sol:TestAuction
[PASS] testPrecisionLossInClaimTokens() (gas: 587714)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.34ms (2.92ms CPU time)
Ran 1 test suite in 10.93ms (5.34ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Users with smaller bids will lose their entire bid amount without receiving any tokens in return.

Tools Used

Manual Review

Recommendations

Update the multiplier calculation:

uint256 tokenDecimals = IERC20Metadata(address(auctionToken)).decimals();
uint256 precision = 10**tokenDecimals;
multiplier = totalTokens.mul(precision).div(totalBids);

Update the claimable token calculation:

uint256 claimable = userBids.mul(multiplier).div(precision);
Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Low decimal tokens or super small bids can lead to 0 claims

Support

FAQs

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