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

Due to lack of validation, a malicious bidder can receive the **full** (`totalTokens`) amount of the `auctionToken` by spending (bidding) just `1 wei` of his/her BJB token (`fjordPoints`) as a cost

Summary

Due to lack of validation, a malicious bidder can receive the full (totalTokens) amount of the auctionToken by spending (bidding) just 1 wei of his/her BJB token (fjordPoints) as a cost.

Vulnerability Details

When a new FjordAuction contract would be deployed via the FjordAuctionFactory#createAuction(), a given _totalTokens would be set to the totalTokens global variable like this:
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L124
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L136

constructor(
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens ///<--------- @audit
) {
...
fjordPoints = ERC20Burnable(_fjordPoints);
auctionToken = IERC20(_auctionToken);
owner = msg.sender;
auctionEndTime = block.timestamp.add(_biddingTime);
totalTokens = _totalTokens; ///<--------- @audit
}

Once the auction would get started, a bidder would bid via the FjordAuction#bid().
Within the FjordAuction#bid(), a given bidding amount would be added to the totalBids.
Then, the given bidding amount of the BJB token (fjordPoints) would be transferred to the FjordAuction contract (address(this)) like this:
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L143
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L149
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L151

function bid(uint256 amount) external { ///<------- @audit
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
bids[msg.sender] = bids[msg.sender].add(amount);
totalBids = totalBids.add(amount); ///<------- @audit
fjordPoints.transferFrom(msg.sender, address(this), amount); ///<------- @audit

Once the current time (block.timestamp) would elapse the auctionEndTime, any user can call the FjordAuction#auctionEnd() to end the auction.
Within the FjordAuction#auctionEnd(), if there was no bids (totalBids == 0), the total amount (totalTokens) of the auctionToken would be transferred back to the FjordAuction contract's owner.
If there were bids (totalBids > 0), the multiplier would be calculated and the pointsToBurn of the BJB token (fjordPoints) would be burned from the FjordAuction contract (address(this)) like this:
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L182-L184
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L192-L195
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#L201

/**
* @notice Ends the auction and calculates claimable tokens for each bidder based on their bid proportion.
*/
function auctionEnd() external {
if (block.timestamp < auctionEndTime) { ///<------- @audit
revert AuctionNotYetEnded();
}
if (ended) {
revert AuctionEndAlreadyCalled();
}
ended = true;
emit AuctionEnded(totalBids, totalTokens);
if (totalBids == 0) { ///<--------- @audit - If there is no bids
auctionToken.transfer(owner, totalTokens); ///<--------- @audit
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); ///<--------- @audit
}

Finally, bidders can receive the claimable amount of the auctionToken by calling the FjordAuction#claimTokens() like this:
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L217
https://github.com/Cyfrin/2024-08-fjord/blob/main/src/FjordAuction.sol#L220

function claimTokens() external {
...
uint256 userBids = bids[msg.sender];
...
uint256 claimable = userBids.mul(multiplier).div(PRECISION_18); ///<--------- @audit
bids[msg.sender] = 0;
auctionToken.transfer(msg.sender, claimable); ///<--------- @audit
...

However,

  • within the FjordAuction#bid() above, there is no requirement of the minimum bids amount for a given bidding amount, which is assigned by a bidder (msg.sender).

  • within the FjordAuction#auctionEnd() above, there is no validation to check whether or not the totalBids exceeds the minimum total bids amount that the auction was valid (successful).

If there is no bids (totalBids == 0) until the last minutes and then some user would call the FjordAuction#auctionEnd(), a malicious bidder would monitor the user's TX of the FjordAuction#auctionEnd() and the malicious bidder would **front-run** it with calling the FjordAuction#bid()with1 wei of the BJB token (fjordPoints`).

As a result, the malicious bidder can get the full (totalTokens) amount of the auctionToken by spending (bidding) just 1 wei of his/her BJB token (fjordPoints) as a cost.

Attack scenario

Let's say Alice is a normal user and Bob is a malicious bidder:

  • 1/ A new FjordAuction contract would be deployed and then the auction would get started.

    • At this time, the totalTokens (of the auctionToken) would be set.

  • 2/ For a while since the auction has gotten started, there has been no bids (totalBids == 0).

  • 3/ Eventually, the current time (block.timestamp) would elapse the auctionEndTime (block.timestamp > auctionEndTime)

  • 4/ Alice would see the situation of the step 3/ and then she would call the FjordAuction#auctionEnd().

  • 5/ Bob would monitor the Alice's TX of the step 4/ and he would front-run it with calling the FjordAuction#bid() with 1 wei as a bidding amount.

  • 6/ Bob's TX (step 5/) would be executed first.

  • 7/ Alice's TX (step 4/) would be executed.

  • 8/ Bob would call the FjordAuction#claimTokens() and receive the whole amount (totalTokens) of the auctionToken.

As you can see the scenario above, since the totalTokens (of the auctionToken) was set initially (when the step 1/) and the Bob's TX would be executed first (when the step 6), Bob could receive the whole amount (totalTokens) of the auctionToken by spending just 1 wei of his BJB token (fjordPoints) as a cost.

Impact

The malicious bidder can get the full (totalTokens) amount of the auctionToken by spending (bidding) just 1 wei of his/her BJB token (fjordPoints) as a cost.

Tools Used

  • Foundry

Recommendations

Within the FjordAuction#bids(), consider adding a validation to check whether or not a given bidding amount exceeds the minimum bidding amount.

Or,

Within the FjordAuction#auctionEnd(), consider adding a validation to check whether or not the totalBids exceeds the minimum total bidding amount that the auction was valid (successful).

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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