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

After the auction ends, a small amount of token dust may remain in the `FjordAuction` contract address.

Summary

After the auction ends, a small amount of token dust may remain in the FjordAuction contract address.

Vulnerability Details

In the FjordAuction::auctionEnd() function, if the totalTokens cannot be evenly divided by the totalBids, a small amount of token dust remains locked in the FjordAuction contract address. The relevant portion of the code is marked with @> below:

// FjordAuction::auctionEnd()
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);
// Burn the FjordPoints held by the contract
uint256 pointsToBurn = fjordPoints.balanceOf(address(this));
fjordPoints.burn(pointsToBurn);
}

Poc

Please place the PoC code into the /test directory and execute it:

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import "../src/FjordStaking.sol";
import { FjordPoints } from "../src/FjordPoints.sol";
import { Test,Vm,console } from "forge-std/Test.sol";
import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol";
import {AuctionFactory} from "../src/FjordAuctionFactory.sol";
import {FjordAuction} from "../src/FjordAuction.sol";
contract PocDust is Test {
AuctionFactory public auctionFactory;
FjordPointsWithMint public fjordPoints;
MockERC20 public auctionToken;
address public owner = makeAddr("owner");
uint256 auctionAmount = 12e18;
function setUp() public {
// deploy auctionToken
auctionToken = new MockERC20("ATOKEN","ATOKEN",18);
vm.startPrank(owner);
// deploy fjordPoints
fjordPoints = new FjordPointsWithMint();
// deploy AuctionFactory
auctionFactory = new AuctionFactory(address(fjordPoints));
vm.stopPrank();
// check owner
assertEq(owner,auctionFactory.owner());
assertEq(owner,fjordPoints.owner());
// Mint auctionToken for owner
auctionToken.mint(owner,100e18);
assertEq(auctionToken.balanceOf(owner),100e18);
}
function test_FjordAuction_dust() public {
// deploy FjordAuction
address auctionAddress = owner_createAuction();
// user calls bid()
uint256 bidAmount = 0.33e18;
for (uint256 i = 1; i < 33; ++i) {
// mint fjordPoints -> user
FjordPointsWithMint(fjordPoints).mint(address(uint160(i)),bidAmount);
vm.startPrank(address(uint160(i)));
fjordPoints.approve(address(auctionAddress),bidAmount);
FjordAuction(auctionAddress).bid(bidAmount);
vm.stopPrank();
}
// check auctionAddress state
assertEq(FjordAuction(auctionAddress).totalBids(),bidAmount * 32);
assertEq(FjordAuction(auctionAddress).totalTokens(),auctionAmount);
vm.warp(11);
FjordAuction(auctionAddress).auctionEnd();
// user calls claimTokens()
for (uint256 i = 1; i < 33; ++i) {
vm.prank(address(uint160(i)));
FjordAuction(auctionAddress).claimTokens();
}
if (auctionToken.balanceOf(address(auctionAddress)) > 0) {
console.log("Dust");
}
if (auctionToken.balanceOf(address(auctionAddress)) == 0) {
console.log("No dust");
}
assertEq(fjordPoints.balanceOf(address(auctionAddress)),0);
}
// [PASS] test_FjordAuction_dust() (gas: 3603785)
// Logs:
// Dust
// helper
function owner_createAuction() public returns (address auctionAddress) {
vm.startPrank(owner);
auctionToken.approve(address(auctionFactory),auctionAmount);
// Capture log
vm.recordLogs();
auctionFactory.createAuction(address(auctionToken),10,auctionAmount,""); // owner transfer 10e18 to auctionAddress
vm.stopPrank();
// Get the recorded log
Vm.Log[] memory logs = vm.getRecordedLogs();
// Traverse the log to find the AuctionCreated event
for (uint256 i = 0; i < logs.length; i++) {
// Signature hash of the event: keccak256("AuctionCreated(address)")
if (logs[i].topics[0] == keccak256("AuctionCreated(address)")) {
// Get the event parameter auctionAddress, which is usually logs[i].topics[1] or logs[i].data[0]
auctionAddress = address(uint160(uint256(logs[i].topics[1]))); // Event parameter address
break;
}
}
}
}
contract FjordPointsWithMint is FjordPoints {
function mint(address account, uint256 amount) public {
_mint(account, amount);
}
}

output:

[PASS] test_FjordAuction_dust() (gas: 3603785)
Logs:
Dust

Code Snippet

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

Impact

If the auction tokens are not evenly divisible by the total bids, leftover token dust may remain in the FjordAuction contract

Tools Used

Manual Review

Recommendations

To prevent token dust from remaining in the contract, consider updating the totalBids after each claimTokens() call. If totalBids becomes zero, transfer any remaining tokens to the owner.

function claimTokens() external {
if (!ended) {
revert AuctionNotYetEnded();
}
uint256 userBids = bids[msg.sender];
if (userBids == 0) {
revert NoTokensToClaim();
}
uint256 claimable = userBids.mul(multiplier).div(PRECISION_18);
bids[msg.sender] = 0;
auctionToken.transfer(msg.sender, claimable);
emit TokensClaimed(msg.sender, claimable);
+ totalBids = totalBids.sub(userBids);
+ if ( totalBids == 0 ) {
+ auctionToken.transfer(owner, auctionToken.balanceOf(address(this)));
+ }
}

output:

[PASS] test_FjordAuction_dust() (gas: 3688244)
Logs:
No dust
Updates

Lead Judging Commences

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

FjordAuction doesn't handle the dust remained after everyone claimed

Support

FAQs

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