Skip to content

Fix/english auction fee on transfer#63

Open
Rav1Chauhan wants to merge 2 commits intoStabilityNexus:mainfrom
Rav1Chauhan:fix/english-auction-fee-on-transfer
Open

Fix/english auction fee on transfer#63
Rav1Chauhan wants to merge 2 commits intoStabilityNexus:mainfrom
Rav1Chauhan:fix/english-auction-fee-on-transfer

Conversation

@Rav1Chauhan
Copy link

@Rav1Chauhan Rav1Chauhan commented Feb 24, 2026

Addressed Issues:

Fixes #62

Summary

This PR fixes accounting and validation issues related to ERC20 fee-on-transfer (deflationary) tokens in EnglishAuction.

Previously:

bid() validated using bidAmount

But state updates used actualReceived

This allowed underbidding if tokens deducted transfer fees

Now:

Validation is performed using actualReceived

State updates occur only after successful validation

Refund logic uses stored previousHighest

receiveFunds now propagates actual received amount

Strict rejection of non-exact token deposits added

Screenshots/Recordings:

Additional Notes:

Checklist

  • [x ] My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • [x ] My code follows the project's code style and conventions.
  • [x ] If applicable, I have made corresponding changes or additions to the documentation.
  • [ x] If applicable, I have made corresponding changes or additions to tests.
  • [x ] My changes generate no new warnings or errors.
  • [ x] I have joined the Stability Nexus's Discord server and I will share a link to this PR with the project maintainers there.
  • [ x] I have read the Contribution Guidelines.
  • [x ] Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

AI Usage Disclosure

Check one of the checkboxes below:

  • This PR does not contain AI-generated code at all.
  • [x ] This PR contains AI-generated code. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools: TODO

⚠️ AI Notice - Important!

We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for fee-on-transfer tokens across all auction types.
    • New event emitted when auctions are created, providing detailed auction information.
  • Bug Fixes

    • Improved tracking of actual token amounts received to prevent discrepancies with fee-on-transfer tokens.
    • Enhanced refund logic for outbid participants.
    • Strengthened protocol fee distribution mechanisms.
  • Improvements

    • Added validation for minimum bid amounts and bid increments.
    • Better state management for auction winners and available funds.

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Walkthrough

Implements fee-on-transfer token support across all auction contracts using balance-difference accounting. Adds bidding invariant enforcement in EnglishAuction (minimumBid > 0, minBidDelta > 0), refunds previous bidders on new bids, and improves reentrancy safety in VickreyAuction by updating state before external transfers.

Changes

Cohort / File(s) Summary
EnglishAuction Core
contracts/EnglishAuction.sol
Introduces AuctionCreated event, enforces minimumBid > 0 and minBidDelta > 0 validation, captures actual received amounts via receiveERC20, refunds previous bidders on new bids, extends deadline after valid bids, distributes protocol fees to treasury, and adds claim/withdrawal improvements.
Reverse Dutch Auction Variants
contracts/ExponentialReverseDutchAuction.sol, contracts/LinearReverseDutchAuction.sol, contracts/LogarithmicReverseDutchAuction.sol
Updates bid flow to use actualReceived from receiveERC20 for availableFunds and settlePrice state updates instead of raw currentPrice, ensuring fee-on-transfer token compatibility.
Vickrey Auction
contracts/VickreyAuction.sol
Refactors revealBid to capture actualReceived via receiveERC20, updates winner/bid logic based on actual amounts, improves reentrancy safety by updating state before external refunds, and adjusts commit-fee handling.
Abstract Auction Base
contracts/abstract/Auction.sol
Extends receiveFunds to return actualReceived; updates receiveERC20 to compute actualReceived via balance-difference accounting and reject fee-on-transfer tokens via strict mode; reorganizes fund-handling sections with documentation headers.
Mock Contracts
contracts/mocks/FeeOnTransferToken.sol
New ERC20 mock contract implementing 10% transfer fee on all token transfers by splitting amounts to recipient and burn address; used for testing fee-on-transfer token scenarios.
Test Suite
test/EnglishAuction.test.ts
Substantial refactoring of auction creation and bidding tests; adds FeeOnTransferToken import; introduces new fee-on-transfer token handling test scenarios; simplifies test structure and updates error message assertions.

Sequence Diagram

sequenceDiagram
    actor Bidder
    participant EnglishAuction
    participant ERC20Token
    participant PreviousBidder

    Bidder->>EnglishAuction: bid(auctionId, amount)
    
    EnglishAuction->>ERC20Token: balanceOf(address(this))
    ERC20Token-->>EnglishAuction: balanceBefore
    
    Bidder->>ERC20Token: approve(EnglishAuction, amount)
    ERC20Token-->>Bidder: approval confirmed
    
    EnglishAuction->>ERC20Token: safeTransferFrom(Bidder, address(this), amount)
    ERC20Token-->>EnglishAuction: transfer executed (may apply fee)
    
    EnglishAuction->>ERC20Token: balanceOf(address(this))
    ERC20Token-->>EnglishAuction: balanceAfter
    
    note over EnglishAuction: actualReceived = balanceAfter - balanceBefore
    note over EnglishAuction: Validate: actualReceived >= minimumBid or minBidDelta
    
    alt Higher Bid Received
        note over EnglishAuction: Store previousHighest bidder
        EnglishAuction->>EnglishAuction: Update highestBid to actualReceived
        EnglishAuction->>EnglishAuction: Update winner to Bidder
        EnglishAuction->>PreviousBidder: Refund previousHighest amount
    end
    
    note over EnglishAuction: Extend deadline
    EnglishAuction-->>Bidder: bidPlaced event emitted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #9 — Removes per-bidder bid storage in EnglishAuction and switches to single highestBid/winner model using actual received amounts for ERC20-based refunds.
  • PR #7 — Moves fund receipt earlier in auction flow and uses actual received amounts in bid/state updates across multiple auction contracts.
  • PR #38 — Updates VickreyAuction.revealBid refund mechanics and state ordering to prevent reentrancy vulnerabilities.

Suggested labels

Solidity Lang, Typescript Lang

Suggested reviewers

  • ceilican
  • yogesh0509

Poem

🐰 Fee tokens hop and skip away,
But balance math saves the day!
Actual amounts, before and after,
No more slippage—hooray and laughter!
Refunds flow like morning dew, 🌧️
Auctions fair and strong and true. ✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing EnglishAuction to handle fee-on-transfer tokens properly.
Linked Issues check ✅ Passed The PR addresses all objectives from issue #62: balance-difference accounting for fee-on-transfer tokens, strict bidding invariants (minimumBid > 0, minBidDelta > 0), and improved refund logic.
Out of Scope Changes check ✅ Passed All changes are directly related to supporting fee-on-transfer tokens and enforcing auction invariants. Additional Dutch Auction variants and Vickrey implementations also apply the same fix patterns, maintaining consistency.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 3

♻️ Duplicate comments (2)
contracts/LinearReverseDutchAuction.sol (1)

125-133: ⚠️ Potential issue | 🟠 Major

Same underbid risk as in ExponentialReverseDutchAuction.bid.
Please apply the same invariant check here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/LinearReverseDutchAuction.sol` around lines 125 - 133, The bid
function may record a winner and settlePrice even if receiveERC20 transfers
fewer tokens than the expected currentPrice, creating an underbid invariant; add
the same check used in ExponentialReverseDutchAuction.bid after calling
receiveERC20 to ensure actualReceived >= currentPrice and revert if not, so that
AuctionData (winner, availableFunds, settlePrice) and the subsequent
claim(auctionId) only execute when the received amount meets or exceeds
getCurrentPrice(auctionId).
contracts/LogarithmicReverseDutchAuction.sol (1)

209-217: ⚠️ Potential issue | 🟠 Major

Same underbid risk as in ExponentialReverseDutchAuction.bid.
Please apply the same invariant check here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/LogarithmicReverseDutchAuction.sol` around lines 209 - 217, In
bid() add the same invariant check used in ExponentialReverseDutchAuction.bid to
ensure the amount returned by receiveERC20 meets the current price: after
computing currentPrice via getCurrentPrice(auctionId) and calling
receiveERC20(auction.biddingToken, msg.sender, currentPrice),
require(actualReceived >= currentPrice, "Insufficient bid"); then set
auction.availableFunds and auction.settlePrice appropriately (use currentPrice
as the settlePrice and handle/refund any excess if actualReceived >
currentPrice) before calling claim(auctionId); reference symbols: bid,
getCurrentPrice, receiveERC20, auction.availableFunds, auction.settlePrice,
claim.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/abstract/Auction.sol`:
- Around line 92-108: The require in receiveFunds unconditionally rejects
fee‑on‑transfer ERC20s; change receiveFunds to accept a boolean flag (e.g.,
allowFeeOnTransfer) or provide a separate receiveFundsAllowFeeOnTransfer variant
so callers can opt in; update the implementation to skip the strict equality
check when allowFeeOnTransfer is true (keep current behavior when false), and
update all call sites that create auctions to pass the appropriate flag (or call
the new variant) so deflationary auction tokens are allowed only when explicitly
requested; reference functions receiveFunds and receiveERC20 when locating and
modifying the logic.

In `@contracts/ExponentialReverseDutchAuction.sol`:
- Around line 211-219: The bid flow currently sets auction.winner and uses
actualReceived from receiveERC20 without ensuring actualReceived meets the
required price, allowing fee-on-transfer tokens to underpay; update the
bid(uint256 auctionId) logic (around functions bid, getCurrentPrice,
receiveERC20, and the fields auction.availableFunds/auction.settlePrice) to
verify after the transfer that actualReceived >= currentPrice and revert with a
clear error if not (or alternatively accept overpayment by reading
msg.value/extra param and ensuring the post-transfer receipts cover
currentPrice), then only set availableFunds/settlePrice and call
claim(auctionId) when the invariant holds.

In `@contracts/mocks/FeeOnTransferToken.sol`:
- Around line 6-22: The 10% fee in FeeOnTransferToken::_update is a magic
number; introduce a named constant (e.g., uint256 constant FEE_RATE = 10 and
optionally uint256 constant FEE_DENOMINATOR = 100 or use BASIS_POINTS = 10000)
and replace the literal 10 and 100 in the fee calculation with those constants
so the fee logic is self-documenting and easier to change; update any comments
to reference the constant and keep the computation as fee = (value * FEE_RATE) /
FEE_DENOMINATOR and use amountAfterFee as before.

---

Duplicate comments:
In `@contracts/LinearReverseDutchAuction.sol`:
- Around line 125-133: The bid function may record a winner and settlePrice even
if receiveERC20 transfers fewer tokens than the expected currentPrice, creating
an underbid invariant; add the same check used in
ExponentialReverseDutchAuction.bid after calling receiveERC20 to ensure
actualReceived >= currentPrice and revert if not, so that AuctionData (winner,
availableFunds, settlePrice) and the subsequent claim(auctionId) only execute
when the received amount meets or exceeds getCurrentPrice(auctionId).

In `@contracts/LogarithmicReverseDutchAuction.sol`:
- Around line 209-217: In bid() add the same invariant check used in
ExponentialReverseDutchAuction.bid to ensure the amount returned by receiveERC20
meets the current price: after computing currentPrice via
getCurrentPrice(auctionId) and calling receiveERC20(auction.biddingToken,
msg.sender, currentPrice), require(actualReceived >= currentPrice, "Insufficient
bid"); then set auction.availableFunds and auction.settlePrice appropriately
(use currentPrice as the settlePrice and handle/refund any excess if
actualReceived > currentPrice) before calling claim(auctionId); reference
symbols: bid, getCurrentPrice, receiveERC20, auction.availableFunds,
auction.settlePrice, claim.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eea3b08 and 365e96c.

📒 Files selected for processing (8)
  • contracts/EnglishAuction.sol
  • contracts/ExponentialReverseDutchAuction.sol
  • contracts/LinearReverseDutchAuction.sol
  • contracts/LogarithmicReverseDutchAuction.sol
  • contracts/VickreyAuction.sol
  • contracts/abstract/Auction.sol
  • contracts/mocks/FeeOnTransferToken.sol
  • test/EnglishAuction.test.ts

Comment on lines +92 to +108
function receiveFunds(
bool isNFT,
address token,
address from,
uint256 tokenIdOrAmount
) internal returns (uint256 actualReceived) {
if (isNFT) {
receiveNFT(token, from, tokenIdOrAmount);
actualReceived = tokenIdOrAmount;
} else {
receiveERC20(token, from, tokenIdOrAmount);
actualReceived = receiveERC20(token, from, tokenIdOrAmount);

// Strict mode: reject deflationary tokens
require(
actualReceived == tokenIdOrAmount,
"Auction: fee-on-transfer tokens not supported"
);
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

Strict equality check blocks fee‑on‑transfer auctioned tokens.
receiveFunds always reverts when actualReceived != tokenIdOrAmount, which prevents creating auctions where the auctioned ERC20 is deflationary. If fee‑on‑transfer support is intended for auctioned assets too, make strictness explicit per call site (or remove the check and enforce only where required).

💡 One way to make strictness configurable
-    function receiveFunds(
-        bool isNFT,
-        address token,
-        address from,
-        uint256 tokenIdOrAmount
-    ) internal returns (uint256 actualReceived) {
+    function receiveFunds(
+        bool isNFT,
+        address token,
+        address from,
+        uint256 tokenIdOrAmount,
+        bool strict
+    ) internal returns (uint256 actualReceived) {
         if (isNFT) {
             receiveNFT(token, from, tokenIdOrAmount);
             actualReceived = tokenIdOrAmount;
         } else {
             actualReceived = receiveERC20(token, from, tokenIdOrAmount);
 
-            // Strict mode: reject deflationary tokens
-            require(
-                actualReceived == tokenIdOrAmount,
-                "Auction: fee-on-transfer tokens not supported"
-            );
+            if (strict) {
+                require(
+                    actualReceived == tokenIdOrAmount,
+                    "Auction: fee-on-transfer tokens not supported"
+                );
+            }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/abstract/Auction.sol` around lines 92 - 108, The require in
receiveFunds unconditionally rejects fee‑on‑transfer ERC20s; change receiveFunds
to accept a boolean flag (e.g., allowFeeOnTransfer) or provide a separate
receiveFundsAllowFeeOnTransfer variant so callers can opt in; update the
implementation to skip the strict equality check when allowFeeOnTransfer is true
(keep current behavior when false), and update all call sites that create
auctions to pass the appropriate flag (or call the new variant) so deflationary
auction tokens are allowed only when explicitly requested; reference functions
receiveFunds and receiveERC20 when locating and modifying the logic.

Comment on lines 211 to 219
function bid(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
AuctionData storage auction = auctions[auctionId];
auction.winner = msg.sender;
uint256 currentPrice = getCurrentPrice(auctionId);
receiveERC20(auction.biddingToken, msg.sender, currentPrice);
auction.availableFunds = currentPrice;
auction.settlePrice = currentPrice;
uint256 actualReceived = receiveERC20(auction.biddingToken, msg.sender, currentPrice);

auction.availableFunds = actualReceived;
auction.settlePrice = actualReceived;
claim(auctionId);
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

Price invariant not enforced with fee‑on‑transfer bids.
actualReceived can be lower than currentPrice, so a bidder can win below the advertised price curve. If the price must be honored, add a post‑transfer check (or extend the API to allow overpayment to cover transfer fees).

🛠️ Suggested invariant check
         uint256 currentPrice = getCurrentPrice(auctionId);
         uint256 actualReceived = receiveERC20(auction.biddingToken, msg.sender, currentPrice);
+        require(actualReceived >= currentPrice, "Auction: bid below current price");
 
         auction.availableFunds = actualReceived;
         auction.settlePrice = actualReceived;
📝 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
function bid(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
AuctionData storage auction = auctions[auctionId];
auction.winner = msg.sender;
uint256 currentPrice = getCurrentPrice(auctionId);
receiveERC20(auction.biddingToken, msg.sender, currentPrice);
auction.availableFunds = currentPrice;
auction.settlePrice = currentPrice;
uint256 actualReceived = receiveERC20(auction.biddingToken, msg.sender, currentPrice);
auction.availableFunds = actualReceived;
auction.settlePrice = actualReceived;
claim(auctionId);
function bid(uint256 auctionId) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) {
AuctionData storage auction = auctions[auctionId];
auction.winner = msg.sender;
uint256 currentPrice = getCurrentPrice(auctionId);
uint256 actualReceived = receiveERC20(auction.biddingToken, msg.sender, currentPrice);
require(actualReceived >= currentPrice, "Auction: bid below current price");
auction.availableFunds = actualReceived;
auction.settlePrice = actualReceived;
claim(auctionId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/ExponentialReverseDutchAuction.sol` around lines 211 - 219, The bid
flow currently sets auction.winner and uses actualReceived from receiveERC20
without ensuring actualReceived meets the required price, allowing
fee-on-transfer tokens to underpay; update the bid(uint256 auctionId) logic
(around functions bid, getCurrentPrice, receiveERC20, and the fields
auction.availableFunds/auction.settlePrice) to verify after the transfer that
actualReceived >= currentPrice and revert with a clear error if not (or
alternatively accept overpayment by reading msg.value/extra param and ensuring
the post-transfer receipts cover currentPrice), then only set
availableFunds/settlePrice and call claim(auctionId) when the invariant holds.

Comment on lines +6 to +22
contract FeeOnTransferToken is ERC20 {

constructor() ERC20("FeeToken", "FEE") {
_mint(msg.sender, 1_000_000 ether);
}

function _update(address from, address to, uint256 value) internal override {
if (from != address(0) && to != address(0)) {
// Apply 10% fee on transfers (not mint/burn)
uint256 fee = (value * 10) / 100;
uint256 amountAfterFee = value - fee;

super._update(from, to, amountAfterFee);
super._update(from, address(0xdead), fee);
} else {
// Minting or burning
super._update(from, to, value);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract the fee rate into a constant to avoid magic numbers.

♻️ Proposed refactor
 contract FeeOnTransferToken is ERC20 {
+    uint256 private constant FEE_BPS = 1000; // 10%
+    uint256 private constant BPS_DENOMINATOR = 10_000;
 
     constructor() ERC20("FeeToken", "FEE") {
         _mint(msg.sender, 1_000_000 ether);
     }
 
     function _update(address from, address to, uint256 value) internal override {
         if (from != address(0) && to != address(0)) {
             // Apply 10% fee on transfers (not mint/burn)
-            uint256 fee = (value * 10) / 100;
+            uint256 fee = (value * FEE_BPS) / BPS_DENOMINATOR;
             uint256 amountAfterFee = value - fee;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/mocks/FeeOnTransferToken.sol` around lines 6 - 22, The 10% fee in
FeeOnTransferToken::_update is a magic number; introduce a named constant (e.g.,
uint256 constant FEE_RATE = 10 and optionally uint256 constant FEE_DENOMINATOR =
100 or use BASIS_POINTS = 10000) and replace the literal 10 and 100 in the fee
calculation with those constants so the fee logic is self-documenting and easier
to change; update any comments to reference the constant and keep the
computation as fee = (value * FEE_RATE) / FEE_DENOMINATOR and use amountAfterFee
as before.

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.

1 participant