-
-
Notifications
You must be signed in to change notification settings - Fork 15
Fix/english auction bid invariants #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e78fcb0
8b99109
1749658
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; | |
| * @notice Auction contract for NFT and token auctions, where the highest bidder wins the auction and rest of the bidders get their bid refunded. | ||
| */ | ||
| contract EnglishAuction is Auction { | ||
| constructor (address _protocolParametersAddress) Auction(_protocolParametersAddress){} | ||
| constructor(address _protocolParametersAddress) Auction(_protocolParametersAddress) {} | ||
| mapping(uint256 => AuctionData) public auctions; | ||
| struct AuctionData { | ||
| uint256 id; | ||
|
|
@@ -65,6 +65,8 @@ contract EnglishAuction is Auction { | |
| uint256 deadlineExtension | ||
| ) external nonEmptyString(name) nonZeroAddress(auctionedToken) nonZeroAddress(biddingToken) { | ||
| require(duration > 0, 'Duration must be greater than zero seconds'); | ||
| require(minimumBid > 0, "minimumBid must be > 0"); | ||
| require(minBidDelta > 0, 'minBidDelta must be > 0'); | ||
| receiveFunds(auctionType == AuctionType.NFT, auctionedToken, msg.sender, auctionedTokenIdOrAmount); | ||
| uint256 deadline = block.timestamp + duration; | ||
| auctions[auctionCounter] = AuctionData({ | ||
|
|
@@ -87,23 +89,52 @@ contract EnglishAuction is Auction { | |
| isClaimed: false, | ||
| protocolFee: protocolParameters.fee() | ||
| }); | ||
| emit AuctionCreated(auctionCounter++, name, description, imgUrl, msg.sender, auctionType, auctionedToken, auctionedTokenIdOrAmount, biddingToken, minimumBid, minBidDelta, deadline, deadlineExtension, protocolParameters.fee()); | ||
| emit AuctionCreated( | ||
| auctionCounter++, | ||
| name, | ||
| description, | ||
| imgUrl, | ||
| msg.sender, | ||
| auctionType, | ||
| auctionedToken, | ||
| auctionedTokenIdOrAmount, | ||
| biddingToken, | ||
| minimumBid, | ||
| minBidDelta, | ||
| deadline, | ||
| deadlineExtension, | ||
| protocolParameters.fee() | ||
| ); | ||
| } | ||
|
|
||
| function bid(uint256 auctionId, uint256 bidAmount) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) { | ||
| AuctionData storage auction = auctions[auctionId]; | ||
| require(auction.highestBid != 0 || bidAmount >= auction.minimumBid, 'First bid should be greater than starting bid'); | ||
| require(auction.highestBid == 0 || bidAmount >= auction.highestBid + auction.minBidDelta, 'Bid amount should exceed current bid by atleast minBidDelta'); | ||
|
|
||
| // First bid must meet minimumBid | ||
| if (auction.highestBid == 0) { | ||
| require(bidAmount >= auction.minimumBid, 'Bid below minimum'); | ||
| } else { | ||
| // Enforce strict increment | ||
| require(bidAmount >= auction.highestBid + auction.minBidDelta, 'Bid increment too low'); | ||
| } | ||
|
|
||
| require(auction.minBidDelta > 0, 'Invalid minBidDelta'); | ||
|
Comment on lines
+113
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Remove redundant minBidDelta runtime check to save gas. ♻️ Suggested change- require(auction.minBidDelta > 0, 'Invalid minBidDelta');🤖 Prompt for AI Agents |
||
|
|
||
| receiveERC20(auction.biddingToken, msg.sender, bidAmount); | ||
|
|
||
| uint256 refund = auction.highestBid; | ||
| address previousWinner = auction.winner; | ||
|
|
||
| auction.winner = msg.sender; | ||
| auction.highestBid = bidAmount; | ||
| auction.availableFunds = bidAmount; | ||
|
|
||
| if (refund != 0) { | ||
| sendERC20(auction.biddingToken, previousWinner, refund); | ||
| } | ||
| auction.availableFunds = bidAmount; | ||
|
|
||
| auction.deadline += auction.deadlineExtension; | ||
|
|
||
| emit bidPlaced(auctionId, msg.sender, bidAmount); | ||
| } | ||
|
|
||
|
|
@@ -114,7 +145,7 @@ contract EnglishAuction is Auction { | |
| uint256 fees = (auction.protocolFee * withdrawAmount) / 10000; | ||
| address feeRecipient = protocolParameters.treasury(); | ||
| sendERC20(auction.biddingToken, auction.auctioneer, withdrawAmount - fees); | ||
| sendERC20(auction.biddingToken,feeRecipient,fees); | ||
| sendERC20(auction.biddingToken, feeRecipient, fees); | ||
| emit Withdrawn(auctionId, withdrawAmount); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.28; | ||
|
|
||
| import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
|
|
||
| contract ERC20Mock is ERC20 { | ||
| constructor( | ||
| string memory name, | ||
| string memory symbol, | ||
| address initialAccount, | ||
| uint256 initialBalance | ||
| ) ERC20(name, symbol) { | ||
| _mint(initialAccount, initialBalance); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| const { expect } = require('chai'); | ||
| const { ethers } = require('hardhat'); | ||
|
|
||
| describe('EnglishAuction - Bidding Invariants', function () { | ||
| let auction; | ||
| let owner, seller, bidder1, bidder2; | ||
| let token; | ||
| let protocol; | ||
|
|
||
| beforeEach(async function () { | ||
| [owner, seller, bidder1, bidder2] = await ethers.getSigners(); | ||
|
|
||
| const MockERC20 = await ethers.getContractFactory('ERC20Mock'); | ||
| token = await MockERC20.deploy('MockToken', 'MTK', owner.address, ethers.parseEther('1000000')); | ||
|
|
||
| const ProtocolMock = await ethers.getContractFactory('ProtocolParameters'); | ||
| protocol = await ProtocolMock.deploy(owner.address, owner.address, 500); | ||
|
|
||
| const EnglishAuction = await ethers.getContractFactory('EnglishAuction'); | ||
| auction = await EnglishAuction.deploy(protocol.target); | ||
|
|
||
| // Give seller tokens | ||
| await token.transfer(seller.address, ethers.parseEther('1000')); | ||
|
|
||
| // Give bidders tokens | ||
| await token.transfer(bidder1.address, ethers.parseEther('1000')); | ||
| await token.transfer(bidder2.address, ethers.parseEther('1000')); | ||
|
|
||
| // Seller must approve for escrow | ||
| await token.connect(seller).approve(auction.target, ethers.parseEther('1000')); | ||
|
|
||
| // Bidders approve for bidding | ||
| await token.connect(bidder1).approve(auction.target, ethers.parseEther('1000')); | ||
|
|
||
| await token.connect(bidder2).approve(auction.target, ethers.parseEther('1000')); | ||
| }); | ||
|
|
||
| async function createAuction(minBidDelta = ethers.parseEther('1')) { | ||
| await auction.connect(seller).createAuction( | ||
| 'Test', | ||
| 'Test Desc', | ||
| 'img', | ||
| 1, // Token auction | ||
| token.target, | ||
| ethers.parseEther('100'), // amount escrowed (mock) | ||
| token.target, | ||
| ethers.parseEther('10'), // minimumBid | ||
| minBidDelta, | ||
| 3600, | ||
| 0, | ||
| ); | ||
| } | ||
|
|
||
| it('1️⃣ First bid must be >= minimumBid', async function () { | ||
| await createAuction(); | ||
|
|
||
| await expect(auction.connect(bidder1).bid(0, ethers.parseEther('5'))).to.be.revertedWith('Bid below minimum'); | ||
|
|
||
| await expect(auction.connect(bidder1).bid(0, ethers.parseEther('10'))).to.not.be.reverted; | ||
| }); | ||
|
|
||
| it('2️⃣ Subsequent bid must be >= highestBid + minBidDelta', async function () { | ||
| await createAuction(ethers.parseEther('2')); | ||
|
|
||
| await auction.connect(bidder1).bid(0, ethers.parseEther('10')); | ||
|
|
||
| await expect(auction.connect(bidder2).bid(0, ethers.parseEther('11'))).to.be.revertedWith('Bid increment too low'); | ||
|
|
||
| await expect(auction.connect(bidder2).bid(0, ethers.parseEther('12'))).to.not.be.reverted; | ||
| }); | ||
|
|
||
| it('3️⃣ Equal bid should revert', async function () { | ||
| await createAuction(ethers.parseEther('1')); | ||
|
|
||
| await auction.connect(bidder1).bid(0, ethers.parseEther('10')); | ||
|
|
||
| await expect(auction.connect(bidder2).bid(0, ethers.parseEther('10'))).to.be.reverted; | ||
| }); | ||
|
Comment on lines
+72
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Make the revert reason explicit for clarity. ✅ Suggested change- await expect(auction.connect(bidder2).bid(0, ethers.parseEther('10'))).to.be.reverted;
+ await expect(auction.connect(bidder2).bid(0, ethers.parseEther('10'))).to.be.revertedWith('Bid increment too low');🤖 Prompt for AI Agents |
||
|
|
||
| it('4️⃣ minBidDelta == 0 should revert', async function () { | ||
| await expect(createAuction(0)).to.be.revertedWith('minBidDelta must be > 0'); | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| it("5️⃣ minimumBid == 0 should revert", async function () { | ||
| await expect( | ||
| auction.connect(seller).createAuction( | ||
| "Test", | ||
| "Test Desc", | ||
| "img", | ||
| 1, | ||
| token.target, | ||
| ethers.parseEther("100"), | ||
| token.target, | ||
| 0, // minimumBid = 0 | ||
| ethers.parseEther("1"), | ||
| 3600, | ||
| 0 | ||
| ) | ||
| ).to.be.revertedWith("minimumBid must be > 0"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First-bid detection can be bypassed when
minimumBidis 0.With
highestBid == 0as the sole sentinel, a zero first bid leaveshighestBidat 0, so subsequent bids are still treated as “first bids” and can ignoreminBidDelta. That reintroduces equal-bid replacement and spam risk whenminimumBidis zero.Proposed fix (use auctioneer sentinel)
As per coding guidelines: "Review for common smart contract vulnerabilities, including but not limited to improper input validation."
🤖 Prompt for AI Agents