Summary
The whenActive
modifier in the Auction
contract uses a strict greater than (>
) comparison when checking if the current block timestamp is after the auction startTime
. This creates a condition where the auction cannot be participated in exactly at the intended start time.
Vulnerability Details
The use of strict comparison >
instead of >=
creates a one-block period after the intended start time where the auction cannot be accessed. The contract's pricing function (getPrice()
) is designed to handle transactions at exactly the start time. This suggests that the contract was designed to work at the start time, but the modifier accidentally prevents it.
modifier whenActive() {
@> require(block.timestamp > state.startTime, "Auction not started");
require(block.timestamp < state.endTime, "Auction ended");
_;
}
Impact
Users cannot participate in the auction exactly at the start time due to this unnecessary one-block delay in auction participation causing missed opportunities for early participants (for example: those who use a bot/automatic system to access the auctions).
Add Foundry to the project following this procedure: .
In the test folder create a file named Auction.t.sol and copy / paste this code:
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {Auction} from "../contracts/zeno/Auction.sol";
import {ZENO} from "../contracts/zeno/ZENO.sol";
import {MockUSDC} from "../contracts/mocks/core/tokens/MockUSDC.sol";
contract AuctionTest is Test {
Auction public auction;
ZENO public zeno;
MockUSDC public mockUSDC;
uint256 public MATURITY_DATE = 86400 * 365;
address public businessAddress = makeAddr("businessAddress");
uint256 public startTime = 1733730100;
uint256 public endTime = 1738832238;
uint256 public startingPrice = 100;
uint256 public reservePrice = 10;
uint256 public totalZENOAllocated = 10;
address public initialOwner = address(this);
function setUp() public {
mockUSDC = new MockUSDC(1000000);
zeno = new ZENO(address(mockUSDC), MATURITY_DATE, "ZEN", "ZENO", address(this));
auction = new Auction(
address(zeno),
address(mockUSDC),
businessAddress,
startTime,
endTime,
startingPrice,
reservePrice,
totalZENOAllocated,
initialOwner
);
console2.log("mockUSDC: ", address(mockUSDC));
console2.log("zenoAddress: ", address(zeno));
console2.log("auction: ", address(auction));
zeno.transferOwnership(address(auction));
}
function test_BidCantStartOnStartTime() public {
vm.warp(1733730100);
address buyer = makeAddr("buyer");
mockUSDC.approve(address(this), type(uint256).max);
mockUSDC.transferFrom(address(this), buyer, 100);
uint256 buyerBalance = mockUSDC.balanceOf(buyer);
assertEq(buyerBalance, 100);
vm.startPrank(buyer);
mockUSDC.approve(address(auction), type(uint256).max);
vm.expectRevert();
auction.buy(1);
vm.stopPrank();
assertEq(zeno.balanceOf(buyer), 0);
}
}
Run: forge test --match-test test_BidCantStartOnStartTime -vvvv
Ran 1 test for test/Auction.t.sol:AuctionTest
[PASS] test_BidCantStartOnStartTime() (gas: 108967)
Logs:
mockUSDC: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
zenoAddress: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
auction: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
Traces:
[108967] AuctionTest::test_BidCantStartOnStartTime()
├─ [0] VM::warp(1733730100 [1.733e9])
│ └─ ← [Return]
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02]
├─ [0] VM::label(buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02], "buyer")
│ └─ ← [Return]
├─ [24735] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::approve(AuctionTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ ├─ emit Approval(owner: AuctionTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], spender: AuctionTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ └─ ← [Return] true
├─ [30421] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::transferFrom(AuctionTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02], 100)
│ ├─ emit Transfer(from: AuctionTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02], value: 100)
│ └─ ← [Return] true
├─ [559] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::balanceOf(buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02]) [staticcall]
│ └─ ← [Return] 100
├─ [0] VM::assertEq(100, 100) [staticcall]
│ └─ ← [Return]
├─ [0] VM::startPrank(buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02])
│ └─ ← [Return]
├─ [24735] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::approve(0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ ├─ emit Approval(owner: buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02], spender: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ └─ ← [Return] true
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [2520] 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a::buy(1)
│ └─ ← [Revert] revert: Auction not started
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [2637] 0x2e234DAe75C793f67A35089C9d99245E1C58470b::balanceOf(buyer: [0x0fF93eDfa7FB7Ad5E962E4C0EdB9207C03a0fe02]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.48ms (269.92µs CPU time)
The test shows that the user cant bid in the auction because the auction state is "not started".
Tools Used
Manual review
Recommendations
Modify the whenActive
modifier to use >=
for the start time check:
whenActive() {
- require(block.timestamp > state.startTime, "Auction not started");
+ require(block.timestamp >= state.startTime, "Auction not started");
require(block.timestamp < state.endTime, "Auction ended");
_;
}