Skip to content

Added auction cancellation functionality and related tests across auctions#39

Open
VishwajeetTulse wants to merge 11 commits intoStabilityNexus:mainfrom
VishwajeetTulse:main
Open

Added auction cancellation functionality and related tests across auctions#39
VishwajeetTulse wants to merge 11 commits intoStabilityNexus:mainfrom
VishwajeetTulse:main

Conversation

@VishwajeetTulse
Copy link

@VishwajeetTulse VishwajeetTulse commented Feb 5, 2026

Feature: Auction Cancellation Functionality

Summary

Implements auction cancellation functionality across all 6 auction types, allowing auctioneers to cancel their auctions before any bids are placed. This addresses the issue of locked capital and poor UX when auctioneers make mistakes or market conditions change.

Problem Statement

Issue: Auctioneers cannot cancel auctions once created, even with zero bids

Impact:

  • Assets locked until auction deadline expires
  • Poor UX - honest mistakes (wrong price, wrong asset, typos) cannot be corrected
  • Capital inefficiency - no way to "delist" an auction with zero interest
  • Forced to wait entire auction duration to reclaim assets via claim()

Changes

Smart Contracts Modified (6 files)

Core Functionality Added to Each Auction Type:

  1. cancelAuction() function - Allows auctioneers to cancel their auctions
  2. AuctionCancelled event - Emits transparent on-chain record of cancellation
  3. Enhanced validation - Added notClaimed modifier to bid/commit functions where missing
Contract Cancel Condition Timing Constraint Lines Changed
EnglishAuction.sol highestBid == 0 Before deadline +19
AllPayAuction.sol highestBid == 0 Before deadline +19
VickreyAuction.sol Before any commits Before bidCommitEnd +16
LinearReverseDutchAuction.sol winner == auctioneer Before deadline +19
ExponentialReverseDutchAuction.sol winner == auctioneer Before deadline +19
LogarithmicReverseDutchAuction.sol winner == auctioneer Before deadline +19

Tests Added (6 files)

Comprehensive test coverage for each auction type:

  • ✅ Auctioneer can cancel NFT auction before any bids
  • ✅ Auctioneer can cancel Token auction before any bids
  • ✅ Non-auctioneer cannot cancel auction
  • ✅ Cannot cancel after bid is placed
  • ✅ Cannot cancel after deadline
  • ✅ Cannot cancel already cancelled auction
  • ✅ Cannot bid/commit on cancelled auction

Total Tests Added: 34 new test cases
Test Status: ✅ All 71 tests passing

Security Considerations

Access Control

  • ✅ Only auction creator (auctioneer) can cancel
  • ✅ Verified via msg.sender == auction.auctioneer

State Management

  • ✅ Auction marked as isClaimed = true to prevent further interactions
  • ✅ Cannot cancel after any bidding activity
  • ✅ Cannot cancel after deadline expires
  • ✅ Asset immediately returned to auctioneer via existing sendFunds() helper

Issue Fixed

#28 Implement Auction Cancellation for Zero-Bid Auctions to Prevent Locked Assets

Summary by CodeRabbit

  • New Features

    • Auctioneers can cancel auctions before any bids (or after deadline if still no bids); assets/funds are returned, auctions marked claimed, and an AuctionCancelled event is emitted. Cancellation is restricted to the auctioneer.
  • Bug Fixes

    • Cancelled auctions block further bids and enforce correct claimed/deadline state to prevent reuse.
  • Tests

    • Comprehensive cancellation test suites added across all auction types covering access control, timing, refunds, events, and post-cancel behavior.

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds an AuctionCancelled event and cancelAuction(auctionId) API across multiple auction contracts; cancellation is restricted to the auctioneer when there are no bids/commits, marks auction claimed, sets deadline to now, returns assets/funds via existing send logic, and blocks further bids.

Changes

Cohort / File(s) Summary
Abstract / event
contracts/abstract/Auction.sol
Added event AuctionCancelled(uint256 indexed auctionId, address indexed auctioneer).
Auction contracts — cancelAuction added
contracts/AllPayAuction.sol, contracts/EnglishAuction.sol, contracts/ExponentialReverseDutchAuction.sol, contracts/LinearReverseDutchAuction.sol, contracts/LogarithmicReverseDutchAuction.sol, contracts/VickreyAuction.sol
Added cancelAuction(uint256 auctionId) that requires caller == auctioneer and no bids/commits, sets isClaimed and deadline = block.timestamp, transfers assets/funds back to auctioneer via existing send logic, and emits AuctionCancelled.
Bid precondition change
contracts/LogarithmicReverseDutchAuction.sol
Removed notClaimed(...) from bid() signature (now only exists + beforeDeadline); ensure cancellation state prevents bidding via deadline/isClaimed handling.
Tests — cancellation coverage
test/AllPayAuction.test.ts, test/EnglishAuction.test.ts, test/ExponentialReverseDutchAuction.test.ts, test/LinearReverseDutchAuction.test.ts, test/LogarithmicReverseDutchAuction.test.ts, test/VickreyAuction.test.ts
Added comprehensive "Auction Cancellation" suites covering successful cancels (NFT & token), event emission, asset/fund returns, isClaimed/deadline handling, and negative cases (non-auctioneer, after bid/commit, after deadline, already cancelled, blocked subsequent bids).

Sequence Diagram(s)

sequenceDiagram
  participant Auctioneer as Auctioneer
  participant Contract as AuctionContract
  participant Asset as ERC20/ERC721

  Auctioneer->>Contract: cancelAuction(auctionId)
  Note right of Contract: validate exists(auctionId)\ncaller == auctioneer\nno bids/commits
  Contract->>Contract: set isClaimed = true\nset deadline = block.timestamp
  Contract->>Asset: sendFunds / transfer NFT back to auctioneer
  Asset-->>Auctioneer: funds / NFT returned
  Contract-->>Auctioneer: emit AuctionCancelled(auctionId, auctioneer)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ceilican
  • yogesh0509

Poem

🐇
I nudged the ledger, quiet and small,
No bids to tumble — I take it all.
The token hops home, the gavel sleeps,
A gentle thump, the meadow keeps.
I nibble clover, cancel complete.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: adding auction cancellation functionality and tests across multiple auction contract types.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@contracts/VickreyAuction.sol`:
- Around line 147-154: The cancelAuction function allows the auctioneer to
cancel even after bidders have committed, which can strand commit fees; update
the AuctionData (or add a new commit counter/flag) to track whether any commits
have been made (e.g., totalCommits or hasCommits) and modify cancelAuction to
require that no commits exist before cancelling (use auction.totalCommits == 0
or !auction.hasCommits), or alternatively implement logic to iterate committed
bidders and refund their commit fees before returning the asset; ensure the
check or refund logic references cancelAuction, AuctionData, and whatever
commit-tracking storage (e.g., commit mapping or counter) so cancellations only
happen safely.
🧹 Nitpick comments (6)
test/AllPayAuction.test.ts (1)

474-474: Consider specifying the expected revert message for consistency.

Other tests in this suite use .to.be.revertedWith('...') to assert specific error messages. Using the generic .to.be.reverted here makes the test less precise and could mask unexpected revert reasons.

🔧 Suggested fix
-            await expect(allPayAuction.connect(bidder1).bid(0, bidAmount)).to.be.reverted;
+            await expect(allPayAuction.connect(bidder1).bid(0, bidAmount)).to.be.revertedWith(
+                'Auctioned asset has already been claimed',
+            );
test/LogarithmicReverseDutchAuction.test.ts (1)

134-239: Missing test cases for full cancellation coverage.

This test suite is missing several scenarios that are covered in AllPayAuction.test.ts and EnglishAuction.test.ts:

  • Token auction cancellation (verifying token refunds)
  • Preventing double-cancellation (already cancelled auction)
  • Preventing bids on cancelled auctions

Adding these would ensure consistent coverage across all auction types.

🧪 Suggested additional test cases
        it('should allow auctioneer to cancel token auction before any bids', async function () {
            const amount = ethers.parseEther('10');
            await mockToken.connect(auctioneer).approve(await logarithmicReverseDutchAuction.getAddress(), amount);

            await logarithmicReverseDutchAuction
                .connect(auctioneer)
                .createAuction(
                    'Token Auction',
                    'Test Description',
                    'https://example.com/test.jpg',
                    1,
                    await mockToken.getAddress(),
                    amount,
                    await biddingToken.getAddress(),
                    ethers.parseEther('10'),
                    ethers.parseEther('1'),
                    20000,
                    10,
                );

            const balanceBefore = await mockToken.balanceOf(await auctioneer.getAddress());
            await logarithmicReverseDutchAuction.connect(auctioneer).cancelAuction(0);
            const balanceAfter = await mockToken.balanceOf(await auctioneer.getAddress());
            expect(balanceAfter).to.equal(balanceBefore + amount);
        });

        it('should not allow cancellation of already cancelled auction', async function () {
            await mockNFT.connect(auctioneer).approve(await logarithmicReverseDutchAuction.getAddress(), 1);
            await logarithmicReverseDutchAuction
                .connect(auctioneer)
                .createAuction(
                    'Test Auction',
                    'Test Description',
                    'https://example.com/test.jpg',
                    0,
                    await mockNFT.getAddress(),
                    1,
                    await biddingToken.getAddress(),
                    ethers.parseEther('10'),
                    ethers.parseEther('1'),
                    20000,
                    10,
                );

            await logarithmicReverseDutchAuction.connect(auctioneer).cancelAuction(0);
            await expect(logarithmicReverseDutchAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith(
                'Auctioned asset has already been claimed',
            );
        });

        it('should not allow bidding on cancelled auction', async function () {
            await mockNFT.connect(auctioneer).approve(await logarithmicReverseDutchAuction.getAddress(), 1);
            await logarithmicReverseDutchAuction
                .connect(auctioneer)
                .createAuction(
                    'Test Auction',
                    'Test Description',
                    'https://example.com/test.jpg',
                    0,
                    await mockNFT.getAddress(),
                    1,
                    await biddingToken.getAddress(),
                    ethers.parseEther('10'),
                    ethers.parseEther('1'),
                    20000,
                    10,
                );

            await logarithmicReverseDutchAuction.connect(auctioneer).cancelAuction(0);

            const currentPrice = await logarithmicReverseDutchAuction.getCurrentPrice(0);
            await biddingToken.connect(bidder1).approve(await logarithmicReverseDutchAuction.getAddress(), currentPrice);
            await expect(logarithmicReverseDutchAuction.connect(bidder1).bid(0)).to.be.revertedWith(
                'Auctioned asset has already been claimed',
            );
        });
test/EnglishAuction.test.ts (1)

490-490: Consider specifying the expected revert message for consistency.

Similar to the pattern used elsewhere in this file, specify the expected revert reason to make the test more precise.

🔧 Suggested fix
-            await expect(englishAuction.connect(bidder1).bid(0, bidAmount)).to.be.reverted;
+            await expect(englishAuction.connect(bidder1).bid(0, bidAmount)).to.be.revertedWith(
+                'Auctioned asset has already been claimed',
+            );
test/ExponentialReverseDutchAuction.test.ts (1)

125-230: Missing test cases for full cancellation coverage.

This test suite is missing several scenarios that are covered in other auction test files:

  • Token auction cancellation (verifying token refunds)
  • Preventing double-cancellation (already cancelled auction)
  • Preventing bids on cancelled auctions

Adding these would ensure consistent coverage across all auction types.

🧪 Suggested additional test cases
        it('should allow auctioneer to cancel token auction before any bids', async function () {
            const amount = ethers.parseEther('10');
            await mockToken.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), amount);

            await exponentialReverseDutchAuction
                .connect(auctioneer)
                .createAuction(
                    'Token Auction',
                    'Test Description',
                    'https://example.com/test.jpg',
                    1,
                    await mockToken.getAddress(),
                    amount,
                    await biddingToken.getAddress(),
                    ethers.parseEther('10'),
                    ethers.parseEther('1'),
                    20000,
                    10,
                );

            const balanceBefore = await mockToken.balanceOf(await auctioneer.getAddress());
            await exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0);
            const balanceAfter = await mockToken.balanceOf(await auctioneer.getAddress());
            expect(balanceAfter).to.equal(balanceBefore + amount);
        });

        it('should not allow cancellation of already cancelled auction', async function () {
            await mockNFT.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), 1);
            await exponentialReverseDutchAuction
                .connect(auctioneer)
                .createAuction(
                    'Test Auction',
                    'Test Description',
                    'https://example.com/test.jpg',
                    0,
                    await mockNFT.getAddress(),
                    1,
                    await biddingToken.getAddress(),
                    ethers.parseEther('10'),
                    ethers.parseEther('1'),
                    20000,
                    10,
                );

            await exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0);
            await expect(exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith(
                'Auctioned asset has already been claimed',
            );
        });

        it('should not allow bidding on cancelled auction', async function () {
            await mockNFT.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), 1);
            await exponentialReverseDutchAuction
                .connect(auctioneer)
                .createAuction(
                    'Test Auction',
                    'Test Description',
                    'https://example.com/test.jpg',
                    0,
                    await mockNFT.getAddress(),
                    1,
                    await biddingToken.getAddress(),
                    ethers.parseEther('10'),
                    ethers.parseEther('1'),
                    20000,
                    10,
                );

            await exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0);

            const currentPrice = await exponentialReverseDutchAuction.getCurrentPrice(0);
            await biddingToken.connect(bidder1).approve(await exponentialReverseDutchAuction.getAddress(), currentPrice);
            await expect(exponentialReverseDutchAuction.connect(bidder1).bid(0)).to.be.revertedWith(
                'Auctioned asset has already been claimed',
            );
        });
test/VickreyAuction.test.ts (2)

299-324: Test description doesn't match implementation.

The test is named "should not allow cancellation after commit phase has started" but it fast-forwards 1100 seconds (past the 1000-second bidCommitDuration), which tests the scenario after the commit phase has ended. The revert message "Cannot cancel: commit phase started or ended" suggests both scenarios should be blocked.

Consider either:

  1. Renaming this test to clarify it tests post-commit-phase cancellation
  2. Adding a separate test that verifies cancellation is blocked once a commit is made (during the commit phase)
🧪 Suggested clarification and additional test
-        it('should not allow cancellation after commit phase has started', async function () {
+        it('should not allow cancellation after commit phase has ended', async function () {
             await mockNFT.connect(auctioneer).approve(await vickreyAuction.getAddress(), 1);
             // ... rest of test
         });
+
+        it('should not allow cancellation once a commit is placed', async function () {
+            await mockNFT.connect(auctioneer).approve(await vickreyAuction.getAddress(), 1);
+            await vickreyAuction
+                .connect(auctioneer)
+                .createAuction(
+                    'Test Auction',
+                    'Test Description',
+                    'https://example.com/test.jpg',
+                    0,
+                    await mockNFT.getAddress(),
+                    1,
+                    await biddingToken.getAddress(),
+                    ethers.parseEther('1'),
+                    1000,
+                    90000,
+                    ethers.parseEther('0.001'),
+                );
+
+            // Place a commit during the commit phase
+            const bid = ethers.parseEther('5');
+            const salt = ethers.encodeBytes32String('secret');
+            const commitment = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'bytes32'], [bid, salt]));
+            await vickreyAuction.connect(bidder1).commitBid(0, commitment, { value: ethers.parseEther('0.001') });
+
+            await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith(
+                'Cannot cancel: commits exist',
+            );
+        });

376-378: Consider specifying the expected revert message for consistency.

Using the generic .to.be.reverted makes the test less precise. Since the auction is cancelled and isClaimed is set to true, the expected revert reason should be verifiable.

🔧 Suggested fix
-            await expect(
-                vickreyAuction.connect(bidder1).commitBid(0, commitment, { value: ethers.parseEther('0.001') }),
-            ).to.be.reverted;
+            await expect(
+                vickreyAuction.connect(bidder1).commitBid(0, commitment, { value: ethers.parseEther('0.001') }),
+            ).to.be.revertedWith('Auctioned asset has already been claimed');

}

function bid(uint256 auctionId, uint256 bidIncrement) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) {
function bid(uint256 auctionId, uint256 bidIncrement) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary since in AllPay asset can only be claimed after deadline so before deadline modifier is sufficient here.

emit Withdrawn(auctionId, withdrawAmount);
}

function cancelAuction(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here also notClaimed is unnecessary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also i just realized checking beforeDeadline is also redundant here as and our only condition is no one has made a bid yet.

uint256 deadlineExtension,
uint256 protocolFee
);
event AuctionCancelled(uint256 indexed auctionId, address indexed auctioneer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This event should be placed in abstract contract since it's same in all contracts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

function bid(uint256 auctionId, uint256 bidAmount) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) {
function bid(uint256 auctionId, uint256 bidAmount) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary here as well.

emit Withdrawn(auctionId, withdrawAmount);
}

function cancelAuction(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here also not necessary of using notClaimed

}

function commitBid(uint256 auctionId, bytes32 commitment) external payable exists(auctionId) beforeDeadline(auctions[auctionId].bidCommitEnd) {
function commitBid(uint256 auctionId, bytes32 commitment) external payable exists(auctionId) beforeDeadline(auctions[auctionId].bidCommitEnd) notClaimed(auctions[auctionId].isClaimed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary here as well since item cannot be claimed before commit period ends.

function cancelAuction(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) {
AuctionData storage auction = auctions[auctionId];
require(msg.sender == auction.auctioneer, "Only auctioneer can cancel");
require(block.timestamp < auction.bidCommitEnd, "Cannot cancel: commit phase started or ended");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here instead of checking time you should check that there are no commits in auction,otherwise someone has initiated the participation and in that case auction shall not be cancelled.

@kaneki003
Copy link
Contributor

@VishwajeetTulse replace beforeDeadline everywhere and our basic condition is to check that no one has initiated the participation in auction.

… notClaimed modifier to prevent bids for cancelled auctions
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/VickreyAuction.test.ts`:
- Around line 299-324: The test titled "should not allow cancellation after
commit phase has started" is misnamed and missing a critical scenario; update
the existing case to reflect it fast-forwards past bidCommitEnd (rename to
"...after commit phase ends") and add a new test that creates an auction, has a
bidder call vickreyAuction.connect(bidder).commitBid(auctionId, commitment, {
value }), then assert that
vickreyAuction.connect(auctioneer).cancelAuction(auctionId) is reverted (expect
revert message matching the contract, e.g., 'Cannot cancel after commits');
reference vickreyAuction, commitBid, and cancelAuction to locate where to add
the new test and reuse the existing auction creation helper code and time window
handling.
🧹 Nitpick comments (2)
contracts/LinearReverseDutchAuction.sol (1)

112-121: Consider using the notClaimed modifier for cross-contract consistency.

EnglishAuction, AllPayAuction, and VickreyAuction use the notClaimed modifier on cancelAuction, while this contract (and LogarithmicReverseDutchAuction) use an inline require. This causes inconsistent user-facing error messages: double-cancelling here reverts with "Deadline of auction reached" (from beforeDeadline) while elsewhere it reverts with "Auctioned asset has already been claimed" (from notClaimed). Using the modifier uniformly improves predictability for integrators and frontend error handling.

Suggested diff
-    function cancelAuction(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) {
+    function cancelAuction(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
         AuctionData storage auction = auctions[auctionId];
         require(msg.sender == auction.auctioneer, "Only auctioneer can cancel");
         require(auction.winner == auction.auctioneer, "Cannot cancel auction with bids");
-        require(!auction.isClaimed, "Auctioned asset has already been claimed");
         auction.isClaimed = true;
contracts/LogarithmicReverseDutchAuction.sol (1)

172-181: Same modifier inconsistency as LinearReverseDutchAuction.

This cancelAuction uses inline require(!auction.isClaimed, ...) while other auction contracts use the notClaimed modifier. Consider unifying for consistent error messages and maintainability (see the comment on LinearReverseDutchAuction.sol for details).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/VickreyAuction.test.ts (1)

492-569: ⚠️ Potential issue | 🟠 Major

Remove duplicate advanceTime and "Reentrancy Regression Tests" block.

Lines 414–491 already define advanceTime and the identical "Reentrancy Regression Tests" suite. This block is a verbatim copy-paste that redeclares the helper (flagged by Biome's noRedeclare lint) and doubles the reentrancy test execution for no benefit.

Proposed fix
-async function advanceTime(seconds: number) {
-    await ethers.provider.send('evm_increaseTime', [seconds]);
-    await ethers.provider.send('evm_mine', []);
-}
-
-   describe('Reentrancy Regression Tests', function () {
-    const BID_COMMIT_DURATION = 1000;
-    const BID_REVEAL_DURATION = 90000;
-    const COMMIT_FEE = ethers.parseEther('0.1');
-    const BID_AMOUNT_1 = ethers.parseEther('10');
-    const BID_AMOUNT_2 = ethers.parseEther('20');
-
-    async function createAuction() {
-      await mockNFT.connect(auctioneer).approve(vickreyAuction.getAddress(), 1);
-      await vickreyAuction.connect(auctioneer).createAuction(
-        'Test NFT',
-        'Test Description',
-        'https://example.com/image.jpg',
-        0,
-        await mockNFT.getAddress(),
-        1,
-        await biddingToken.getAddress(),
-        ethers.parseEther('5'),
-        BID_COMMIT_DURATION,
-        BID_REVEAL_DURATION,
-        COMMIT_FEE
-      );
-      return 0;
-    }
-
-    async function commitBids(auctionId: number) {
-      const salt1 = ethers.id('salt1');
-      const salt2 = ethers.id('salt2');
-
-      const commitment1 = ethers.solidityPackedKeccak256(['uint256', 'bytes32'], [BID_AMOUNT_1, salt1]);
-      const commitment2 = ethers.solidityPackedKeccak256(['uint256', 'bytes32'], [BID_AMOUNT_2, salt2]);
-
-      await vickreyAuction.connect(bidder1).commitBid(auctionId, commitment1, { value: COMMIT_FEE });
-      await vickreyAuction.connect(bidder2).commitBid(auctionId, commitment2, { value: COMMIT_FEE });
-
-      return { salt1, salt2 };
-    }
-
-    it('should update accumulatedCommitFee before external call in revealBid', async function () {
-      const auctionId = await createAuction();
-      const { salt1 } = await commitBids(auctionId);
-
-      await advanceTime(BID_COMMIT_DURATION + 1);
-
-      const before = await vickreyAuction.auctions(auctionId);
-      expect(before.accumulatedCommitFee).to.equal(COMMIT_FEE * 2n);
-      await biddingToken.connect(bidder1).approve(vickreyAuction.getAddress(), BID_AMOUNT_1);
-      await vickreyAuction.connect(bidder1).revealBid(auctionId, BID_AMOUNT_1, salt1);
-
-      const after = await vickreyAuction.auctions(auctionId);
-      expect(after.accumulatedCommitFee).to.equal(COMMIT_FEE);
-    });
-
-    it('should update accumulatedCommitFee to 0 before external call in withdraw', async function () {
-      const auctionId = await createAuction();
-      const { salt1, salt2 } = await commitBids(auctionId);
-
-      await advanceTime(BID_COMMIT_DURATION + 1);
-
-      await biddingToken.connect(bidder1).approve(vickreyAuction.getAddress(), BID_AMOUNT_1);
-      await biddingToken.connect(bidder2).approve(vickreyAuction.getAddress(), BID_AMOUNT_2);
-
-      await vickreyAuction.connect(bidder1).revealBid(auctionId, BID_AMOUNT_1, salt1);
-      await vickreyAuction.connect(bidder2).revealBid(auctionId, BID_AMOUNT_2, salt2);
-
-      await advanceTime(BID_REVEAL_DURATION + 1);
-
-      await vickreyAuction.connect(auctioneer).withdraw(auctionId);
-
-      const after = await vickreyAuction.auctions(auctionId);
-      expect(after.accumulatedCommitFee).to.equal(0);
-    });
-  });
🧹 Nitpick comments (1)
test/VickreyAuction.test.ts (1)

383-411: Test relies on an implementation-specific side-effect rather than an explicit cancellation guard.

The test asserts that committing on a cancelled auction reverts with "Deadline of auction reached", which means the contract blocks bids by manipulating bidCommitEnd rather than checking a dedicated cancellation flag. This is fragile — if the deadline mechanism changes, this test would silently stop covering the cancellation path.

Consider also asserting against a more explicit revert reason (e.g., "Auction is cancelled") if/when the contract adds a dedicated guard, or at minimum add a comment in the test clarifying this coupling.

@@ -148,6 +148,7 @@ contract VickreyAuction is Auction {
AuctionData storage auction = auctions[auctionId];
require(msg.sender == auction.auctioneer, "Only auctioneer can cancel");
require(block.timestamp < auction.bidCommitEnd, "Cannot cancel: commit phase started or ended");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not required then,after checking commitFee is zero.So this line could be removed.

@kaneki003
Copy link
Contributor

@VishwajeetTulse not all the comments above are addressed.Could you look again into them.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@contracts/ExponentialReverseDutchAuction.sol`:
- Around line 134-142: The cancelAuction function is missing the notClaimed
guard and can be called after claim(), enabling double-spend for token auctions;
update cancelAuction (in ExponentialReverseDutchAuction) to enforce the same
protection as LinearReverseDutchAuction by requiring the auction is not already
claimed (use the existing notClaimed modifier or add require(!auction.isClaimed,
...) at the top of cancelAuction) so that sendFunds cannot be invoked twice for
the same auction/winner; keep the rest of the logic (setting isClaimed,
deadline, sendFunds, emit AuctionCancelled) unchanged.

In `@contracts/LinearReverseDutchAuction.sol`:
- Around line 111-119: The cancelAuction function lacks a notClaimed guard
allowing double-spend; fix by preventing cancellation if auction.isClaimed is
true — either add the existing notClaimed modifier to cancelAuction's signature
or insert require(!auction.isClaimed, "Already claimed") near the start of
cancelAuction (after loading AuctionData and confirming auctioneer), so that
sendFunds cannot be executed twice; reference cancelAuction,
AuctionData.isClaimed, notClaimed modifier, exists modifier, and sendFunds when
making the change.

In `@contracts/LogarithmicReverseDutchAuction.sol`:
- Around line 171-179: The cancelAuction function is missing the notClaimed
guard, allowing a double-spend on ERC-20 auctions; update the function signature
for cancelAuction(uint256 auctionId) to include the notClaimed modifier (i.e.,
cancelAuction(uint256 auctionId) external exists(auctionId) notClaimed) so it
checks AuctionData.isClaimed before proceeding, ensuring AuctionData in
auctions[auctionId] cannot be cancelled/claimed twice; keep the existing body
(setting isClaimed, deadline, sendFunds, emitting AuctionCancelled).

In `@test/AllPayAuction.test.ts`:
- Around line 302-454: Add two tests exercising the notClaimed guard: (1)
"double-cancel (token auction)" — create a token auction via createAuction with
mockToken/ERC20, cancelAuction(0) once, then assert a second cancelAuction(0)
call reverts; (2) "claim then cancel" — create a token auction, advance time
past deadline, call claim(0) as the rightful claimer, then assert a subsequent
cancelAuction(0) call reverts. Reference cancelAuction, claim, createAuction,
and the auction index 0 (and mockToken approvals/balance checks) to locate where
to add these specs. Ensure approvals and time travel (evm_increaseTime/evm_mine)
mirror existing token-auction tests.

In `@test/ExponentialReverseDutchAuction.test.ts`:
- Around line 125-230: Add tests covering missing cancellation/edge cases: 1)
create a token-backed auction via createAuction (use biddingToken and token
contract address similar to NFT flow) and assert cancelAuction emits
AuctionCancelled and returns token to auctioneer; 2) ensure double-cancel
protection by calling cancelAuction twice on the same auction id and expecting
the second call to revert (match message used in contract); 3) simulate deadline
passing then call claim(auctionId) and afterwards assert
cancelAuction(auctionId) reverts (or behaves per contract) to cover
claim-then-cancel path; 4) after cancelling an auction, attempt bid(auctionId)
(after approving biddingToken and using getCurrentPrice) and assert bid reverts
to verify bids are blocked on cancelled auctions; mirror patterns/assertions
used in existing tests (e.g., AuctionCancelled event, auctions(0).isClaimed) for
consistency.

In `@test/LinearReverseDutchAuction.test.ts`:
- Around line 237-364: Add three unit tests in the "Auction Cancellation"
describe block to cover double-cancel, claim-then-cancel, and bid-after-cancel
flows: (1) Double-cancel: create a token auction via createAuction, call
cancelAuction once (assert AuctionCancelled emitted and token returned), then
call cancelAuction a second time and assert the call reverts (use
.to.be.reverted) to guard the notClaimed behavior; (2) Claim-then-cancel: create
a token auction, fast-forward past deadline, call claim() (assert
success/claimed state via auctions(0).isClaimed or event), then call
cancelAuction and assert it reverts (.to.be.reverted); (3) Bid-after-cancel:
create an auction, cancel it, then attempt bid(0) (after approving biddingToken)
and assert bid reverts (.to.be.reverted). Use the existing helpers and contracts
(createAuction, cancelAuction, claim, bid, auctions, mockToken/mockNFT,
biddingToken, ethers.provider.send('evm_increaseTime')) and follow the same
setup patterns as the surrounding tests.

In `@test/VickreyAuction.test.ts`:
- Around line 222-412: Add a test that verifies double-cancel is rejected by
creating an auction, calling vickreyAuction.connect(auctioneer).cancelAuction(0)
once, then asserting a second cancelAuction(0) call reverts (matching the
contract revert message) and checking auctions(0).isClaimed remains true; and
add a test that creates an auction, has a bidder call commitBid(0, commitment, {
value: commitFee }), advances time past the commit deadline
(evm_increaseTime/evm_mine) and then asserts that
vickreyAuction.connect(auctioneer).cancelAuction(0) is reverted (same guard as
when commitments exist), referencing cancelAuction, commitBid, and the
auctions(...).isClaimed flag to locate the logic.
🧹 Nitpick comments (5)
contracts/LinearReverseDutchAuction.sol (1)

121-121: Consider restoring notClaimed on bid() for defense-in-depth.

The beforeDeadline check effectively blocks bids after cancellation (since deadline is set to block.timestamp), and claim() internally still has notClaimed. However, keeping notClaimed on bid() provides an explicit, self-documenting safety net against future refactors that might alter the deadline logic.

♻️ Suggested change
-    function bid(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) {
+    function bid(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) beforeDeadline(auctions[auctionId].deadline) {
contracts/ExponentialReverseDutchAuction.sol (1)

144-144: Consider restoring notClaimed on bid() for defense-in-depth.

Same reasoning as the LinearReverseDutchAuction comment — beforeDeadline is sufficient today, but notClaimed adds an explicit guard against future regressions.

♻️ Suggested change
-    function bid(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) {
+    function bid(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) beforeDeadline(auctions[auctionId].deadline) {
contracts/LogarithmicReverseDutchAuction.sol (1)

181-181: Consider restoring notClaimed on bid() for defense-in-depth.

Same reasoning as the other reverse Dutch auction contracts.

♻️ Suggested change
-    function bid(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) {
+    function bid(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) beforeDeadline(auctions[auctionId].deadline) {
test/VickreyAuction.test.ts (2)

448-449: Inconsistent commitment hash encoding across test sections.

The main test suite (line 77) uses ethers.keccak256(AbiCoder.defaultAbiCoder().encode(...)) while the reentrancy tests use ethers.solidityPackedKeccak256(...). These happen to produce the same output here because both uint256 and bytes32 are exactly 32 bytes, but the divergent style is confusing and fragile — if a type changes (e.g., to uint128), the two approaches would silently diverge.

Pick one encoding style and use it consistently.


414-417: advanceTime helper is underutilized.

This helper duplicates the inline evm_increaseTime + evm_mine pattern used throughout the cancellation tests (e.g., lines 318-319, 345-346). Consider moving it above the cancellation describe block and using it there too to reduce boilerplate.

Comment on lines +134 to +142
function cancelAuction(uint256 auctionId) external exists(auctionId) {
AuctionData storage auction = auctions[auctionId];
require(msg.sender == auction.auctioneer, "Only auctioneer can cancel");
require(auction.winner == auction.auctioneer, "Cannot cancel auction with bids");
auction.isClaimed = true;
auction.deadline = block.timestamp; // Set deadline to now, preventing future bids via beforeDeadline modifier
sendFunds(auction.auctionType == AuctionType.NFT, auction.auctionedToken, auction.auctioneer, auction.auctionedTokenIdOrAmount);
emit AuctionCancelled(auctionId, auction.auctioneer);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Same notClaimed guard missing as in LinearReverseDutchAuction — double-spend on ERC-20 token auctions.

The identical exploit path applies: after a no-bid auction expires, claim() followed by cancelAuction() (or two cancelAuction calls for token auctions) drains tokens from the contract's pooled balance.

🐛 Proposed fix — add `notClaimed` modifier
-    function cancelAuction(uint256 auctionId) external exists(auctionId) {
+    function cancelAuction(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) {
🤖 Prompt for AI Agents
In `@contracts/ExponentialReverseDutchAuction.sol` around lines 134 - 142, The
cancelAuction function is missing the notClaimed guard and can be called after
claim(), enabling double-spend for token auctions; update cancelAuction (in
ExponentialReverseDutchAuction) to enforce the same protection as
LinearReverseDutchAuction by requiring the auction is not already claimed (use
the existing notClaimed modifier or add require(!auction.isClaimed, ...) at the
top of cancelAuction) so that sendFunds cannot be invoked twice for the same
auction/winner; keep the rest of the logic (setting isClaimed, deadline,
sendFunds, emit AuctionCancelled) unchanged.

Comment on lines +111 to +119
function cancelAuction(uint256 auctionId) external exists(auctionId) {
AuctionData storage auction = auctions[auctionId];
require(msg.sender == auction.auctioneer, "Only auctioneer can cancel");
require(auction.winner == auction.auctioneer, "Cannot cancel auction with bids");
auction.isClaimed = true;
auction.deadline = block.timestamp; // Set deadline to now, preventing future bids via beforeDeadline modifier
sendFunds(auction.auctionType == AuctionType.NFT, auction.auctionedToken, auction.auctioneer, auction.auctionedTokenIdOrAmount);
emit AuctionCancelled(auctionId, auction.auctioneer);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: cancelAuction is missing a notClaimed guard — enables double-spend on ERC-20 token auctions.

Because cancelAuction never checks isClaimed, an auctioneer can drain tokens belonging to other auctions stored in the same contract:

  1. Create a token auction (ERC-20 transferred into the contract).
  2. Let the deadline pass with no bids (winner == auctioneer remains true).
  3. Call claim(auctionId)isClaimed = true, tokens sent to auctioneer.
  4. Call cancelAuction(auctionId)winner == auctioneer still passes, sendFunds sends the same amount again from the contract's pooled balance.

The same vector applies to calling cancelAuction twice in a row for token auctions.

🐛 Proposed fix — add `notClaimed` modifier
-    function cancelAuction(uint256 auctionId) external exists(auctionId) {
+    function cancelAuction(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) {
🤖 Prompt for AI Agents
In `@contracts/LinearReverseDutchAuction.sol` around lines 111 - 119, The
cancelAuction function lacks a notClaimed guard allowing double-spend; fix by
preventing cancellation if auction.isClaimed is true — either add the existing
notClaimed modifier to cancelAuction's signature or insert
require(!auction.isClaimed, "Already claimed") near the start of cancelAuction
(after loading AuctionData and confirming auctioneer), so that sendFunds cannot
be executed twice; reference cancelAuction, AuctionData.isClaimed, notClaimed
modifier, exists modifier, and sendFunds when making the change.

Comment on lines +171 to +179
function cancelAuction(uint256 auctionId) external exists(auctionId) {
AuctionData storage auction = auctions[auctionId];
require(msg.sender == auction.auctioneer, "Only auctioneer can cancel");
require(auction.winner == auction.auctioneer, "Cannot cancel auction with bids");
auction.isClaimed = true;
auction.deadline = block.timestamp; // Set deadline to now, preventing future bids via beforeDeadline modifier
sendFunds(auction.auctionType == AuctionType.NFT, auction.auctionedToken, auction.auctioneer, auction.auctionedTokenIdOrAmount);
emit AuctionCancelled(auctionId, auction.auctioneer);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Same notClaimed guard missing — double-spend on ERC-20 token auctions.

Same exploit path as LinearReverseDutchAuction and ExponentialReverseDutchAuction. Please add the notClaimed modifier.

🐛 Proposed fix — add `notClaimed` modifier
-    function cancelAuction(uint256 auctionId) external exists(auctionId) {
+    function cancelAuction(uint256 auctionId) external exists(auctionId) notClaimed(auctions[auctionId].isClaimed) {
🤖 Prompt for AI Agents
In `@contracts/LogarithmicReverseDutchAuction.sol` around lines 171 - 179, The
cancelAuction function is missing the notClaimed guard, allowing a double-spend
on ERC-20 auctions; update the function signature for cancelAuction(uint256
auctionId) to include the notClaimed modifier (i.e., cancelAuction(uint256
auctionId) external exists(auctionId) notClaimed) so it checks
AuctionData.isClaimed before proceeding, ensuring AuctionData in
auctions[auctionId] cannot be cancelled/claimed twice; keep the existing body
(setting isClaimed, deadline, sendFunds, emitting AuctionCancelled).

Comment on lines +302 to +454
describe('Auction Cancellation', function () {
it('should allow auctioneer to cancel auction before any bids', async function () {
await mockNFT.connect(auctioneer).approve(await allPayAuction.getAddress(), 1);
await allPayAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('1'),
ethers.parseEther('0.1'),
5,
10,
);

expect(await mockNFT.ownerOf(1)).to.equal(await allPayAuction.getAddress());

await expect(allPayAuction.connect(auctioneer).cancelAuction(0))
.to.emit(allPayAuction, 'AuctionCancelled')
.withArgs(0, await auctioneer.getAddress());

expect(await mockNFT.ownerOf(1)).to.equal(await auctioneer.getAddress());
const auction = await allPayAuction.auctions(0);
expect(auction.isClaimed).to.be.true;
});

it('should allow auctioneer to cancel token auction before any bids', async function () {
const amount = ethers.parseEther('10');
await mockToken.connect(auctioneer).approve(await allPayAuction.getAddress(), amount);

await allPayAuction
.connect(auctioneer)
.createAuction(
'Token Auction',
'Test Description',
'https://example.com/test.jpg',
1,
await mockToken.getAddress(),
amount,
await biddingToken.getAddress(),
ethers.parseEther('1'),
ethers.parseEther('0.1'),
5,
10,
);

const balanceBefore = await mockToken.balanceOf(await auctioneer.getAddress());
await allPayAuction.connect(auctioneer).cancelAuction(0);
const balanceAfter = await mockToken.balanceOf(await auctioneer.getAddress());
expect(balanceAfter).to.equal(balanceBefore + amount);
});

it('should not allow non-auctioneer to cancel auction', async function () {
await mockNFT.connect(auctioneer).approve(await allPayAuction.getAddress(), 1);
await allPayAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('1'),
ethers.parseEther('0.1'),
5,
10,
);

await expect(allPayAuction.connect(bidder1).cancelAuction(0)).to.be.revertedWith('Only auctioneer can cancel');
});

it('should not allow cancellation after bid is placed', async function () {
await mockNFT.connect(auctioneer).approve(await allPayAuction.getAddress(), 1);
await allPayAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('1'),
ethers.parseEther('0.1'),
5,
10,
);

const bidAmount = ethers.parseEther('1.5');
await biddingToken.connect(bidder1).approve(await allPayAuction.getAddress(), bidAmount);
await allPayAuction.connect(bidder1).bid(0, bidAmount);

await expect(allPayAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith('Cannot cancel auction with bids');
});

it('should allow cancellation after deadline if no bids', async function () {
await mockNFT.connect(auctioneer).approve(await allPayAuction.getAddress(), 1);
await allPayAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('1'),
ethers.parseEther('0.1'),
5,
10,
);

await ethers.provider.send('evm_increaseTime', [10]);
await ethers.provider.send('evm_mine', []);

await expect(allPayAuction.connect(auctioneer).cancelAuction(0))
.to.emit(allPayAuction, 'AuctionCancelled')
.withArgs(0, await auctioneer.getAddress());
});

it('should not allow bidding on cancelled auction', async function () {
await mockNFT.connect(auctioneer).approve(await allPayAuction.getAddress(), 1);
await allPayAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('1'),
ethers.parseEther('0.1'),
5,
10,
);

await allPayAuction.connect(auctioneer).cancelAuction(0);

const bidAmount = ethers.parseEther('1.5');
await biddingToken.connect(bidder1).approve(await allPayAuction.getAddress(), bidAmount);
await expect(allPayAuction.connect(bidder1).bid(0, bidAmount)).to.be.revertedWith('Deadline of auction reached');
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add test cases for double-cancel and claim-then-cancel to exercise the notClaimed guard.

The current suite validates the happy path and basic rejections well. However, two important adversarial scenarios are missing:

  1. Double cancel (token auction): Cancel once, then attempt cancelAuction again — should revert (once the notClaimed fix is applied to the contract).
  2. Claim then cancel: Create a token auction, let the deadline pass with no bids, call claim(), then call cancelAuction() — should revert. This path currently exploits the missing notClaimed guard on cancelAuction.

These tests would serve as regression guards for the critical double-spend vulnerability identified in the contract reviews.

🤖 Prompt for AI Agents
In `@test/AllPayAuction.test.ts` around lines 302 - 454, Add two tests exercising
the notClaimed guard: (1) "double-cancel (token auction)" — create a token
auction via createAuction with mockToken/ERC20, cancelAuction(0) once, then
assert a second cancelAuction(0) call reverts; (2) "claim then cancel" — create
a token auction, advance time past deadline, call claim(0) as the rightful
claimer, then assert a subsequent cancelAuction(0) call reverts. Reference
cancelAuction, claim, createAuction, and the auction index 0 (and mockToken
approvals/balance checks) to locate where to add these specs. Ensure approvals
and time travel (evm_increaseTime/evm_mine) mirror existing token-auction tests.

Comment on lines +125 to +230
describe('Auction Cancellation', function () {
it('should allow auctioneer to cancel auction before any bids', async function () {
await mockNFT.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), 1);
await exponentialReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
20000,
10,
);

expect(await mockNFT.ownerOf(1)).to.equal(await exponentialReverseDutchAuction.getAddress());

await expect(exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0))
.to.emit(exponentialReverseDutchAuction, 'AuctionCancelled')
.withArgs(0, await auctioneer.getAddress());

expect(await mockNFT.ownerOf(1)).to.equal(await auctioneer.getAddress());
const auction = await exponentialReverseDutchAuction.auctions(0);
expect(auction.isClaimed).to.be.true;
});

it('should not allow non-auctioneer to cancel auction', async function () {
await mockNFT.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), 1);
await exponentialReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
20000,
10,
);

await expect(exponentialReverseDutchAuction.connect(bidder1).cancelAuction(0)).to.be.revertedWith(
'Only auctioneer can cancel',
);
});

it('should not allow cancellation after bid is placed', async function () {
await mockNFT.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), 1);
await exponentialReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
20000,
10,
);

const currentPrice = await exponentialReverseDutchAuction.getCurrentPrice(0);
await biddingToken.connect(bidder1).approve(await exponentialReverseDutchAuction.getAddress(), currentPrice);
await exponentialReverseDutchAuction.connect(bidder1).bid(0);

await expect(exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith(
'Cannot cancel auction with bids',
);
});

it('should allow cancellation after deadline if no bids', async function () {
await mockNFT.connect(auctioneer).approve(await exponentialReverseDutchAuction.getAddress(), 1);
await exponentialReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
20000,
10,
);

await ethers.provider.send('evm_increaseTime', [15]);
await ethers.provider.send('evm_mine', []);

await expect(exponentialReverseDutchAuction.connect(auctioneer).cancelAuction(0))
.to.emit(exponentialReverseDutchAuction, 'AuctionCancelled')
.withArgs(0, await auctioneer.getAddress());
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Test coverage gaps: missing token-auction cancel, double-cancel, claim-then-cancel, and bid-after-cancel scenarios.

Compared to the AllPayAuction test suite, this file is missing:

  • Token auction cancellation (only NFT is tested).
  • Double-cancel protection (cancel → cancel again).
  • Claim-then-cancel path (deadline passes → claim()cancelAuction()).
  • Bidding on a cancelled auction (verifies the deadline mechanism blocks bids).

These are important given the notClaimed vulnerability in the contract.

🤖 Prompt for AI Agents
In `@test/ExponentialReverseDutchAuction.test.ts` around lines 125 - 230, Add
tests covering missing cancellation/edge cases: 1) create a token-backed auction
via createAuction (use biddingToken and token contract address similar to NFT
flow) and assert cancelAuction emits AuctionCancelled and returns token to
auctioneer; 2) ensure double-cancel protection by calling cancelAuction twice on
the same auction id and expecting the second call to revert (match message used
in contract); 3) simulate deadline passing then call claim(auctionId) and
afterwards assert cancelAuction(auctionId) reverts (or behaves per contract) to
cover claim-then-cancel path; 4) after cancelling an auction, attempt
bid(auctionId) (after approving biddingToken and using getCurrentPrice) and
assert bid reverts to verify bids are blocked on cancelled auctions; mirror
patterns/assertions used in existing tests (e.g., AuctionCancelled event,
auctions(0).isClaimed) for consistency.

Comment on lines +237 to +364
describe('Auction Cancellation', function () {
it('should allow auctioneer to cancel auction before any bids', async function () {
await mockNFT.connect(auctioneer).approve(await linearReverseDutchAuction.getAddress(), 1);
await linearReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
10,
);

expect(await mockNFT.ownerOf(1)).to.equal(await linearReverseDutchAuction.getAddress());

await expect(linearReverseDutchAuction.connect(auctioneer).cancelAuction(0))
.to.emit(linearReverseDutchAuction, 'AuctionCancelled')
.withArgs(0, await auctioneer.getAddress());

expect(await mockNFT.ownerOf(1)).to.equal(await auctioneer.getAddress());
const auction = await linearReverseDutchAuction.auctions(0);
expect(auction.isClaimed).to.be.true;
});

it('should allow auctioneer to cancel token auction before any bids', async function () {
const amount = ethers.parseEther('10');
await mockToken.connect(auctioneer).approve(await linearReverseDutchAuction.getAddress(), amount);

await linearReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Token Auction',
'Test Description',
'https://example.com/test.jpg',
1,
await mockToken.getAddress(),
amount,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
10,
);

const balanceBefore = await mockToken.balanceOf(await auctioneer.getAddress());
await linearReverseDutchAuction.connect(auctioneer).cancelAuction(0);
const balanceAfter = await mockToken.balanceOf(await auctioneer.getAddress());
expect(balanceAfter).to.equal(balanceBefore + amount);
});

it('should not allow non-auctioneer to cancel auction', async function () {
await mockNFT.connect(auctioneer).approve(await linearReverseDutchAuction.getAddress(), 1);
await linearReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
10,
);

await expect(linearReverseDutchAuction.connect(bidder1).cancelAuction(0)).to.be.revertedWith(
'Only auctioneer can cancel',
);
});

it('should not allow cancellation after bid is placed', async function () {
await mockNFT.connect(auctioneer).approve(await linearReverseDutchAuction.getAddress(), 1);
await linearReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
10,
);

const bidAmount = ethers.parseEther('10');
await biddingToken.connect(bidder1).approve(await linearReverseDutchAuction.getAddress(), bidAmount);
await linearReverseDutchAuction.connect(bidder1).bid(0);

await expect(linearReverseDutchAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith(
'Cannot cancel auction with bids',
);
});

it('should allow cancellation after deadline if no bids', async function () {
await mockNFT.connect(auctioneer).approve(await linearReverseDutchAuction.getAddress(), 1);
await linearReverseDutchAuction
.connect(auctioneer)
.createAuction(
'Test Auction',
'Test Description',
'https://example.com/test.jpg',
0,
await mockNFT.getAddress(),
1,
await biddingToken.getAddress(),
ethers.parseEther('10'),
ethers.parseEther('1'),
10,
);

await ethers.provider.send('evm_increaseTime', [15]);
await ethers.provider.send('evm_mine', []);

await expect(linearReverseDutchAuction.connect(auctioneer).cancelAuction(0))
.to.emit(linearReverseDutchAuction, 'AuctionCancelled')
.withArgs(0, await auctioneer.getAddress());
});

});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Good coverage for basic flows; add double-cancel and claim-then-cancel tests.

NFT and token cancellation, authorization, and post-bid rejection are well covered. Please also add:

  • Double cancel: Cancel a token auction twice — should revert after the notClaimed fix.
  • Claim then cancel: Let a no-bid token auction expire, call claim(), then cancelAuction() — should revert.
  • Bid after cancel: Verify bid() reverts on a cancelled auction.

These mirror the gaps identified across the other test suites and guard against the notClaimed vulnerability.

🤖 Prompt for AI Agents
In `@test/LinearReverseDutchAuction.test.ts` around lines 237 - 364, Add three
unit tests in the "Auction Cancellation" describe block to cover double-cancel,
claim-then-cancel, and bid-after-cancel flows: (1) Double-cancel: create a token
auction via createAuction, call cancelAuction once (assert AuctionCancelled
emitted and token returned), then call cancelAuction a second time and assert
the call reverts (use .to.be.reverted) to guard the notClaimed behavior; (2)
Claim-then-cancel: create a token auction, fast-forward past deadline, call
claim() (assert success/claimed state via auctions(0).isClaimed or event), then
call cancelAuction and assert it reverts (.to.be.reverted); (3)
Bid-after-cancel: create an auction, cancel it, then attempt bid(0) (after
approving biddingToken) and assert bid reverts (.to.be.reverted). Use the
existing helpers and contracts (createAuction, cancelAuction, claim, bid,
auctions, mockToken/mockNFT, biddingToken,
ethers.provider.send('evm_increaseTime')) and follow the same setup patterns as
the surrounding tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/VickreyAuction.test.ts`:
- Around line 503-514: The main auction tests use
AbiCoder.defaultAbiCoder().encode(...) to build commitment hashes which pads
values, but the contract's VickreyAuction.revealBid() expects
keccak256(abi.encodePacked(bidAmount, salt)) (tight packing); change the test
code that builds commitments (where AbiCoder.defaultAbiCoder().encode is used)
to use ethers.solidityPackedKeccak256(['uint256','bytes32'], [bidAmount, salt])
(same types/order as used in commit/reveal flow) so the generated commitment
values match the contract; ensure the same salt and BID_AMOUNT variables are
passed and that commitBid/revealBid calls remain unchanged.
🧹 Nitpick comments (2)
test/VickreyAuction.test.ts (2)

431-436: Strengthen the double-cancel assertion with a specific revert reason.

Using .to.be.reverted without a reason string is fragile — the transaction could revert for an unrelated reason and the test would still pass. Assert the specific revert message to ensure the notClaimed/isClaimed guard is what blocks the second call.

♻️ Suggested tightening
-            await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.reverted;
+            await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith(
+                'Auction already claimed',  // adjust to match the actual contract revert string
+            );

473-476: advanceTime helper is defined but only used in Reentrancy Regression Tests.

Consider moving it to a shared test utilities file or using it consistently in the cancellation tests above (which inline evm_increaseTime/evm_mine calls instead).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/VickreyAuction.test.ts`:
- Around line 433-435: The test currently checks double-cancel with a generic
revert; update the assertion to assert the exact revert reason from the contract
(use .to.be.revertedWith(...)) so the failure is tied to the not-claimed
condition. Locate the test line using
vickreyAuction.connect(auctioneer).cancelAuction(0) and replace the generic
.to.be.reverted with .to.be.revertedWith("...") using the exact revert string
emitted by the contract's notClaimed modifier (or the contract's cancelAuction
revert message).
🧹 Nitpick comments (2)
test/VickreyAuction.test.ts (2)

222-471: Extract shared auction-creation helper to reduce duplication.

Every cancellation test repeats the same ~15-line createAuction block. The "Reentrancy Regression Tests" section (line 485) already defines a createAuction() helper. Consider hoisting a similar helper to the outer describe scope (or a shared utility) so both suites and all cancellation tests can reuse it.


473-476: Consider using advanceTime in the cancellation tests too.

This helper is defined at the outer scope but only called in the reentrancy suite. The cancellation tests (lines 318-319, 345-346, 463-464) inline the same evm_increaseTime/evm_mine pattern. Using advanceTime everywhere would be more consistent.

Comment on lines +433 to +435

// Try to cancel again - should revert because asset already claimed
await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.reverted;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert the specific revert reason for double-cancel.

Every other revert assertion in this file uses .to.be.revertedWith(...), but this one only uses .to.be.reverted. This is weaker — it would pass even if the transaction reverts for an unrelated reason. Pin it to the expected message (likely from the notClaimed modifier) for consistency and correctness.

Proposed fix
-            await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.reverted;
+            await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith('Asset already claimed');

Adjust the string to match the actual revert message in the contract.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Try to cancel again - should revert because asset already claimed
await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.reverted;
// Try to cancel again - should revert because asset already claimed
await expect(vickreyAuction.connect(auctioneer).cancelAuction(0)).to.be.revertedWith('Asset already claimed');
🤖 Prompt for AI Agents
In `@test/VickreyAuction.test.ts` around lines 433 - 435, The test currently
checks double-cancel with a generic revert; update the assertion to assert the
exact revert reason from the contract (use .to.be.revertedWith(...)) so the
failure is tied to the not-claimed condition. Locate the test line using
vickreyAuction.connect(auctioneer).cancelAuction(0) and replace the generic
.to.be.reverted with .to.be.revertedWith("...") using the exact revert string
emitted by the contract's notClaimed modifier (or the contract's cancelAuction
revert message).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants