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

bid function doesn't check the balance of the user

Summary

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

/**
* @notice Places a bid in the auction.
* @param amount The amount of FjordPoints to bid.
*/
function bid(uint256 amount) external {
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
bids[msg.sender] = bids[msg.sender].add(amount);
totalBids = totalBids.add(amount);
fjordPoints.transferFrom(msg.sender, address(this), amount);
emit BidAdded(msg.sender, amount);
}

the function bid does not check if the balance is equal or greater than amount. "In this case, the transferFrom operation will fail if the user does not have a sufficient FjordPoints balance, and an error will be thrown. According to the ERC20 standard, the transferFrom function will fail and revert in the following cases:

If the sender (msg.sender) does not have a sufficient balance.
If the sender has not granted sufficient spending allowance to the contract.
Therefore, the call to fjordPoints.transferFrom(msg.sender, address(this), amount); will automatically revert the transaction and throw an error if the user does not have a sufficient balance or spending allowance.

In this scenario, even without an explicit balance check, the transaction will fail in case of insufficient balance. However, this approach has a few disadvantages:

The error message will not be customized, and it may not provide the user with clear information about why the transaction failed.
It may result in unnecessary gas consumption because the transaction will progress until the transferFrom call and only fail at that point.
A better approach would be to check the user's balance at the beginning of the transaction.

Vulnerability Details

Without balance check, there will be more gas consumption

To compare this check, we need to write a test.
First add this function to FjordAuction.sol

function bidWithCheck(uint256 amount) external {
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
if (fjordPoints.balanceOf(msg.sender) < amount) {
revert InsufficientBalance();
}
bids[msg.sender] = bids[msg.sender].add(amount);
totalBids = totalBids.add(amount);
fjordPoints.transferFrom(msg.sender, address(this), amount);
emit BidAdded(msg.sender, amount);
}

also add this error to FjordAuction.sol

error InsufficientBalance();

now add this function to FjordPoints.sol

function mintTest(address to, uint256 amount) external {
_mint(to, amount);
}

now create a test file named FjordAuction.t.sol and add this code

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.21;
import "forge-std/Test.sol";
import "../src/FjordAuction.sol";
import "../src/FjordPoints.sol";
contract GasTest is Test {
FjordAuction auction;
FjordPoints fjordPoints;
address user = address(0x1);
uint256 auctionDuration = 7 days;
uint256 totalTokens = 1000 ether;
function setUp() public {
fjordPoints = new FjordPoints();
auction = new FjordAuction(
address(fjordPoints),
address(0x2),
auctionDuration,
totalTokens
);
fjordPoints.mintTest(user, 100 ether); // We are issuing tokens using a temporary mint function
}
function testGasUsageWithoutCheck() public {
vm.startPrank(user);
fjordPoints.approve(address(auction), 1000 ether);
uint256 gasStart = gasleft();
try auction.bid(1000 ether) {
// This should fail, but we're measuring gas usage
} catch {
// We expect this to fail
}
uint256 gasUsed = gasStart - gasleft();
console.log("Gas used without balance check:", gasUsed);
vm.stopPrank();
}
function testGasUsageWithCheck() public {
vm.startPrank(user);
fjordPoints.approve(address(auction), 1000 ether);
uint256 gasStart = gasleft();
try auction.bidWithCheck(1000 ether) {
// This should fail, but we're measuring gas usage
} catch {
// We expect this to fail
}
uint256 gasUsed = gasStart - gasleft();
console.log("Gas used with balance check:", gasUsed);
vm.stopPrank();
}
}

now, when you start both tests testGasUsageWithCheck and testGasUsageWithoutCheck you will see the difference.

Here you can see testGasUsageWithoutCheck:

Traces:
[98431] GasTest::testGasUsageWithoutCheck()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000001)
│ └─ ← [Return]
├─ [24652] FjordPoints::approve(FjordAuction: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 1000000000000000000000 [1e21])
│ ├─ emit Approval(owner: 0x0000000000000000000000000000000000000001, spender: FjordAuction: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 1000000000000000000000 [1e21])
│ └─ ← [Return] true
├─ [54889] FjordAuction::bid(1000000000000000000000 [1e21])
│ ├─ [5470] FjordPoints::transferFrom(0x0000000000000000000000000000000000000001, FjordAuction: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 1000000000000000000000 [1e21])
│ │ ├─ emit Approval(owner: 0x0000000000000000000000000000000000000001, spender: FjordAuction: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 0)
│ │ └─ ← [Revert] revert: ERC20: transfer amount exceeds balance
│ └─ ← [Revert] revert: ERC20: transfer amount exceeds balance
├─ [0] console::log("Gas used without balance check:", 57867 [5.786e4]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]

now lets see testGasUsageWithCheck:

Traces:
[51089] GasTest::testGasUsageWithCheck()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000001)
│ └─ ← [Return]
├─ [24652] FjordPoints::approve(FjordAuction: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 1000000000000000000000 [1e21])
│ ├─ emit Approval(owner: 0x0000000000000000000000000000000000000001, spender: FjordAuction: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 1000000000000000000000 [1e21])
│ └─ ← [Return] true
├─ [7503] FjordAuction::bidWithCheck(1000000000000000000000 [1e21])
│ ├─ [2608] FjordPoints::balanceOf(0x0000000000000000000000000000000000000001) [staticcall]
│ │ └─ ← [Return] 100000000000000000000 [1e20]
│ └─ ← [Revert] InsufficientBalance()
├─ [0] console::log("Gas used with balance check:", 10481 [1.048e4]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]

When running the test testGasUsageWithoutCheck the usage is 57867 but while running with testGasUsageWithCheck the usage is 10481.

Impact

To calculate the current gas fees, we can follow these steps:

1. Gas Units

a) Gas used without a balance check: 57,867

b) Gas used with a balance check: 10,481

2. Gas Price

The gas price varies depending on network conditions and is usually expressed in gwei. For example, 1 gwei = 0.000000001 ETH. Let's assume the average gas price is 20 gwei (though this may change).

3. ETH Price

To determine the USD equivalent of Ethereum, we need to use the current market price. Let's assume the current ETH/USD price is 1700 USD.

4. Calculation

First, we calculate the total gas cost in ETH.

Then, we convert this amount to USD.

Let's calculate it.

In terms of gas fees:

A transaction without a balance check costs approximately 1.97 USD.

A transaction with a balance check costs approximately 0.36 USD.

This means that, without a balance check, the user pays approximately 1.61 USD more. This shows that you can significantly reduce costs by avoiding
unnecessary gas consumption.

Tools Used

Manual review

Recommendations

A better approach would be to check the user's balance at the beginning of the transaction.

function bid(uint256 amount) external {
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
+ if (fjordPoints.balanceOf(msg.sender) < amount) {
+ revert InsufficientBalance();
+ }
bids[msg.sender] = bids[msg.sender].add(amount);
totalBids = totalBids.add(amount);
fjordPoints.transferFrom(msg.sender, address(this), amount);
emit BidAdded(msg.sender, amount);
}

This way, the user is provided with a clearer error message, and unnecessary gas consumption is avoided.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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