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

The last claimer of the `FjordAuction` will `lose his eligible claim funds`, if fee-on-transfer token is used as the `auctionToken`

Summary

The last claimer of the FjordAuction will lose his eligible claim funds (DoS the FjordAuction.claimTokens() transaction), if fee-on-transfer token is used as the auctionToken.

Vulnerability Details

The FjordAuctionFactory.sol contract is used to create a new auction. After the deployment of the new auction the total token amount for the auction is transferred to the auction contract as shown below:

address auctionAddress = address(
new FjordAuction{ salt: salt }(fjordPoints, auctionToken, biddingTime, totalTokens)
); //@audit-info - creates a new FjordAuction contract with the given parameters using CREATE2
//@audit - Can there be a CREATE2 address collission issue
// Transfer the auction tokens from the msg.sender to the new auction contract
IERC20(auctionToken).transferFrom(msg.sender, auctionAddress, totalTokens);

The totalTokens parameter is used as the FjordAuction.constructor parameter for the auctionToken amount and it is transferred to the auction by calling the IERC20(auctionToken).transferFrom. This passed in totalTokens constructor parameter is subsequently used in the calculations in the FjordAuction.auctionEnd function to calculate the claim token amounts for the bidder.

Now the issue is all these auction tokens are deposited in the Fjord treasury and these are native tokens of the projects which have used to the Fjord protocol. Hence these auction tokens could have the fee on transfer feature in them. As a result when the totalTokens is transferred to the FjordAuction contract fee will be charged an less amount of tokens will be transferred to the auction contract as a result. But the state FjordAuction.totalTokens will still have the initialized totalTokens parameter when the contract was deployed by FjordAuctionFactory.createAuction.

Impact

As a result of the above vulnerability, when the bidders are claiming their auction tokens the last claimer (or last claimers) will not be able to claim his auction token since there is not enough balance of auctionTokens in the FjordAuction contract. Hence this is loss of funds to the last claimer since his FjordAuction.claimTokens transaction will always revert. And his points are also burnt since the FjordStaking.auctionEnd function burns all the FjordPoints at the end of its execution. Hence this is loss of funds to the last claimer of the FjordAuction.

Please add the following testcase to the test/unit/auction.t.sol and execute the following command.

forge test --match-test testClaimTokensFeeOnTransfer

function setUp() public {
fjordPoints = new ERC20BurnableMock("FjordPoints", "fjoPTS");
auctionToken = new ERC20BurnableMock("AuctionToken", "AUCT");
auction =
new FjordAuction(address(fjordPoints), address(auctionToken), biddingTime, totalTokens);
// Due to fee-on-transfer only 999 ether is transferred to the auction contract from the auction factory contract
deal(address(auctionToken), address(auction), 999 ether);
}
function testClaimTokensFeeOnTransfer() public {
address bidderA = address(0x2);
uint256 bidAmountA = 100 ether;
address bidderB = address(0x3);
uint256 bidAmountB = 100 ether;
address bidderC = address(0x4);
uint256 bidAmountC = 100 ether;
deal(address(fjordPoints), bidderA, bidAmountA);
deal(address(fjordPoints), bidderB, bidAmountB);
deal(address(fjordPoints), bidderC, bidAmountC);
//There are three seperate bids in the auction by three bidders
vm.startPrank(bidderA);
fjordPoints.approve(address(auction), bidAmountA);
auction.bid(bidAmountA);
vm.stopPrank();
vm.startPrank(bidderB);
fjordPoints.approve(address(auction), bidAmountB);
auction.bid(bidAmountB);
vm.stopPrank();
vm.startPrank(bidderC);
fjordPoints.approve(address(auction), bidAmountC);
auction.bid(bidAmountC);
vm.stopPrank();
skip(biddingTime);
console.log("Starting Auction Tokens in the Contract: ", auctionToken.balanceOf(address(auction)).div(1e18));
console.log("TotalBids Amount: ", auction.totalBids().div(1e18));
console.log("BidderA Total Bids: ", auction.bids(bidderA).div(1e18));
console.log("BidderB Total Bids: ", auction.bids(bidderB).div(1e18));
console.log("BidderC Total Bids: ", auction.bids(bidderC).div(1e18));
// First to claims are succesful
vm.startPrank(bidderA);
auction.auctionEnd();
auction.claimTokens();
console.log("BidderA claimed Amount: ", auctionToken.balanceOf(bidderA).div(1e18));
vm.stopPrank();
vm.prank(bidderB);
auction.claimTokens();
console.log("BidderB claimed Amount: ", auctionToken.balanceOf(bidderB).div(1e18));
// Last claimer is unable to claim his tokens since there is not enouhg auction token balance in the auction contract
vm.prank(bidderC);
vm.expectRevert("ERC20: transfer amount exceeds balance");
auction.claimTokens();
console.log("BidderC claimed Amount: ", auctionToken.balanceOf(bidderC).div(1e18));
console.log("Remaining Auction Tokens in the Contract: ", auctionToken.balanceOf(address(auction)).div(1e18));
}

https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuctionFactory.sol#L58-L63
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L136
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L197
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L220

Tools Used

Manual Review, Foundry and VSCode

Recommendations

Hence it is recommended to initially calculate the FjordAuction contract address using CREATE 2 in the FjordAuctionFactory.createAuction function and then transfer the totalTokens amount of the auctionToken to the FjordAuction contract. Then the FjordAuction contract can be deployed at the predetermined auction address and then when the FjordAuction.totalTokens is initialized the auction token balance of the FjordAuction contract can be used as the amount of totalTokens for the auction.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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