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

`FjordAuction` Fee On Transfer Tokens Will be Stuck/Lost In the Auction Contract

Summary

FjordAuction::auctionEnd and FjordAuction::claimTokens will revert due to the contract token balance being less than the attempted transfer amount.

Vulnerability Details

The following is from the known issue of the contract which I believe does not cover this situation:
Additional Fees: Some ERC20 tokens include mechanisms like transfer fees, burn rates, or automatic redistribution to holders. When such a token is used in the auction, these fees could be inadvertently passed on to users, resulting in higher-than-expected costs.

PoC

Place the following contract into mocks folder with the filename ERC20FeeOnTransferMock.sol

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import { ERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract ERC20FeeOnTransferMock is ERC20 {
address public constant FEE_ADDRESS = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; //Vitalik deserves more
constructor() ERC20("Fee Token", "FOTM") { }
function transferFrom(address from, address to, uint256 amount)
public
virtual
override
returns (bool)
{
address spender = _msgSender();
_spendAllowance(from, spender, amount);
amount -= 100 wei; // to simulate a fee
_transfer(from, FEE_ADDRESS, 100 wei);
_transfer(from, to, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
amount -= 100 wei; // to simulate a fee
_transfer(owner, FEE_ADDRESS, 100 wei);
_transfer(owner, to, amount);
return true;
}
}

Place the following test and import into auction.t.sol

import { ERC20FeeOnTransferMock } from "../mocks/ERC20FeeOnTransferMock.sol";
import { AuctionFactory } from "src/FjordAuctionFactory.sol";

The following test shows what happens when there are no bids:

function test_AuctionTokensGetStuckNoBids() public {
// SETUP:
//Deploy a fee on transfer ERC20 mock contract
ERC20FeeOnTransferMock feeOnTransferToken = new ERC20FeeOnTransferMock();
//Deploy the auction factory
AuctionFactory auctionFactory = new AuctionFactory(address(fjordPoints));
deal(address(feeOnTransferToken), address(this), totalTokens);
feeOnTransferToken.approve(address(auctionFactory), totalTokens);
// noBidding time to se we can the auction right away
uint256 auctionBiddingTime = 0;
bytes32 salt = bytes32(uint256(1234));
//START:
// To get the address of the Auction contract, we need get event data
vm.recordLogs();
auctionFactory.createAuction(
address(feeOnTransferToken), auctionBiddingTime, totalTokens, salt
);
Vm.Log[] memory entries = vm.getRecordedLogs();
console.log("The length of the recorded events is: ", entries.length);
console.log(
"The address of the deployed auction is: ",
address(uint160(uint256(entries[2].topics[1])))
);
address newAuctionAddress = address(uint160(uint256(entries[3].topics[1])));
feeOnTransferToken.balanceOf(newAuctionAddress);
// We retrieved the contract address from the logged event emission
FjordAuction newAuction = FjordAuction(newAuctionAddress);
// give, approve, and bid the points token for bidders
//Skip so to the end of the auction time
skip(1);
vm.expectRevert("ERC20: transfer amount exceeds balance");
newAuction.auctionEnd(); // This should revert
// auction tokens are stuck
}

Next the following test show what will happen when there are bids and user claim their tokens after an auction:

function test_AuctionTokensGetStuckWithBidder() public {
// SETUP:
//Deploy a fee on transfer ERC20 mock contract
ERC20FeeOnTransferMock feeOnTransferToken = new ERC20FeeOnTransferMock();
//Deploy the auction factory
AuctionFactory auctionFactory = new AuctionFactory(address(fjordPoints));
deal(address(feeOnTransferToken), address(this), totalTokens);
feeOnTransferToken.approve(address(auctionFactory), totalTokens);
// noBidding time to se we can the auction right away
uint256 auctionBiddingTime = 5 hours;
bytes32 salt = bytes32(uint256(1234));
//Create some bidding accounts
address[] memory bidders = new address[](4);
bidders[0] = makeAddr("bidder1");
bidders[1] = makeAddr("bidder2");
bidders[2] = makeAddr("bidder3");
bidders[3] = makeAddr("bidder4");
uint256 pointAmount = 100 ether;
//START:
// To get the address of the Auction contract
vm.recordLogs();
auctionFactory.createAuction(
address(feeOnTransferToken), auctionBiddingTime, totalTokens, salt
);
Vm.Log[] memory entries = vm.getRecordedLogs();
console.log("The length of the recorded events is: ", entries.length);
console.log(
"The address of the deployed auction is: ",
address(uint160(uint256(entries[2].topics[1])))
);
address newAuctionAddress = address(uint160(uint256(entries[3].topics[1])));
feeOnTransferToken.balanceOf(newAuctionAddress);
// We retrieved the contract address from the logged event emission
FjordAuction newAuction = FjordAuction(newAuctionAddress);
// give, approve, and bid the points token for bidders
for (uint256 i = 0; i < bidders.length; i++) {
deal(address(fjordPoints), bidders[i], pointAmount);
vm.startPrank(bidders[i]);
fjordPoints.approve(address(newAuctionAddress), pointAmount);
newAuction.bid(pointAmount);
vm.stopPrank();
}
//Skip so to the end of the auction time
skip(5.1 hours);
newAuction.auctionEnd();
//Bidders will claim their tokens
vm.prank(bidders[0]);
newAuction.claimTokens();
vm.prank(bidders[1]);
newAuction.claimTokens();
vm.prank(bidders[2]);
newAuction.claimTokens();
// This should revert
vm.startPrank(bidders[3]);
vm.expectRevert("ERC20: transfer amount exceeds balance");
newAuction.claimTokens();
// auction tokens are stuck
}

Run the test with: forge test --mt test_AuctionTokensGetStuck

Impact

  1. Auction Tokens will be locked in the contract.

  2. If the totalBids == 0 and the block.timestamp < auctionEndTime the auction will never be ended and the owner will not receive a refund of the auction tokens.

  3. After an auction with bids claimTokens will always revert for the last user to claim tokens.

Tools Used

Foundry and Manual Review

Recommendations

In the constructor of FjordAuction set the totalTokens to current balance of the contract

- totalTokens = _totalTokens;
+ totalTokens = auctionToken.getBalance(address(this));

This is will prevent the tokens from being stuck etc. but users will still receive less tokens which is a known issue.

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.