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

Dust Tokens Left Unclaimed After Auctions end

Summary

The FjordAuction contract may leave small amounts of unclaimed tokens (dust) after users claim their rewards due to rounding errors in the token distribution process.

Vulnerability Details

When users call claimTokens from FjordAuction to claim their tokens, rounding down during calculations can leave leftover tokens in the contract:

https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordAuction.sol#L207

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);
}

These small amounts, although minor individually, can accumulate among different FjordAuctions deployed over time and result in tokens being stuck in all the auction contracts.

Proof of code:
Add the following test in ClaimReward_Unit_Test.t.sol:

function test_ClaimReward_DustLeft() public {
//current contract stakes 20 ether making sum of 30 because of the setup method
fjordStaking.stake(20 ether);
//2 weeks of 1 ether per week added as a reward
_addRewardAndEpochRollover(1 ether, 2);
//5 weeks of no rewards added
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration() * 5);
address alice = makeAddr("alice");
deal(address(token), alice, 10000 ether);
//alice stakes
vm.startPrank(alice);
fjordStaking.stake(30 ether);
vm.stopPrank();
//5 weeks of no rewards added
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration() * 5);
//2 weeks of 1 ether per week added as a reward
_addRewardAndEpochRollover(1 ether, 2);
FjordPoints fpt = FjordPoints(points);
fpt.claimPoints();
vm.startPrank(alice);
fpt.claimPoints();
vm.stopPrank();
uint256 totalTokens = 1000 ether;
uint256 biddingTime = 1 weeks;
ERC20BurnableMock auctionToken = new ERC20BurnableMock("AuctionToken", "AUCT");
deal(address(auctionToken), alice, totalTokens);
vm.startPrank(alice);
AuctionFactory auctionFactory = new AuctionFactory(address(fpt));
//alice approves the factory to transfer the tokens to it
auctionToken.approve(address(auctionFactory), totalTokens);
bytes32 salt = keccak256(abi.encodePacked(block.timestamp, msg.sender));
//alice creates an auction and we use the event to get it's address
vm.recordLogs();
auctionFactory.createAuction(address(auctionToken), biddingTime, totalTokens, salt);
Vm.Log[] memory logs = vm.getRecordedLogs();
address auctionAddress;
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == keccak256("AuctionCreated(address)")) {
auctionAddress = address(uint160(uint256(logs[i].topics[1])));
}
}
FjordAuction fjordAuction = FjordAuction(auctionAddress);
//alice bids to the auction
fpt.approve(auctionAddress, fpt.balanceOf(alice));
fjordAuction.bid(fpt.balanceOf(alice));
vm.stopPrank();
//the test contract bids in the auction
fpt.approve(auctionAddress, fpt.balanceOf(address(this)));
fjordAuction.bid(fpt.balanceOf(address(this)));
//biddingTime ends and auctionEnd is performed,
skip(biddingTime);
fjordAuction.auctionEnd();
console.log("======================== DUST AMOUNT LEFT ========================");
//the test contract claims its tokens
fjordAuction.claimTokens();
//alice claims her tokens
vm.startPrank(alice);
fjordAuction.claimTokens();
vm.stopPrank();
//balance left as a dust in the auction after everyone claimed
console.log("auctionToken.balanceOf(auctionAddress) = ", auctionToken.balanceOf(auctionAddress));
}

Impact

(Lock)Loss of dust amounts of auctionTokens from different auctions.

Tools Used

Manual Review

Recommendations

Include a mechanism to handle tokens dust in claimToken function of FjordAuction, ensuring all tokens are properly allocated or returned.

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 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.