-
-
Notifications
You must be signed in to change notification settings - Fork 15
fix: enforce NFT escrow ownership verification before claim #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,15 +6,22 @@ import './ProtocolParameters.sol'; | |
| import '@openzeppelin/contracts/token/ERC721/IERC721.sol'; | ||
| import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; | ||
| import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; | ||
| import "@openzeppelin/contracts/access/Ownable.sol"; | ||
| import "@openzeppelin/contracts/utils/Pausable.sol"; | ||
|
|
||
| /** | ||
| * @title AllPayAuction | ||
| * @notice Auction contract for NFT and token auctions,where all bidders pay their bid amount but only the highest bidder wins the auction. | ||
| */ | ||
| contract AllPayAuction is Auction { | ||
| constructor (address _protocolParametersAddress) Auction(_protocolParametersAddress){} | ||
| mapping(uint256 => AuctionData) public auctions; // auctionId => AuctionData | ||
| mapping(uint256 => mapping(address => uint256)) public bids; // auctionId => (bidder => bidAmount) | ||
| contract AllPayAuction is Auction, Ownable, Pausable { | ||
|
|
||
| constructor(address _protocolParametersAddress) | ||
| Auction(_protocolParametersAddress) | ||
| Ownable(msg.sender) | ||
| {} | ||
|
|
||
| mapping(uint256 => AuctionData) public auctions; | ||
| mapping(uint256 => mapping(address => uint256)) public bids; | ||
|
|
||
| struct AuctionData { | ||
| uint256 id; | ||
| string name; | ||
|
|
@@ -35,6 +42,7 @@ contract AllPayAuction is Auction { | |
| bool isClaimed; | ||
| uint256 protocolFee; | ||
| } | ||
|
|
||
| event AuctionCreated( | ||
| uint256 indexed Id, | ||
| string name, | ||
|
|
@@ -52,6 +60,22 @@ contract AllPayAuction is Auction { | |
| uint256 protocolFee | ||
| ); | ||
|
|
||
| // ----------------------- | ||
| // PAUSE CONTROL | ||
| // ----------------------- | ||
|
|
||
| function pause() external onlyOwner { | ||
| _pause(); | ||
| } | ||
|
|
||
| function unpause() external onlyOwner { | ||
| _unpause(); | ||
| } | ||
|
|
||
| // ----------------------- | ||
| // CREATE AUCTION | ||
| // ----------------------- | ||
|
|
||
| function createAuction( | ||
| string memory name, | ||
| string memory description, | ||
|
|
@@ -64,10 +88,24 @@ contract AllPayAuction is Auction { | |
| uint256 minBidDelta, | ||
| uint256 duration, | ||
| uint256 deadlineExtension | ||
| ) external nonEmptyString(name) nonZeroAddress(auctionedToken) nonZeroAddress(biddingToken) { | ||
| require(duration > 0, 'Duration should be greater than 0'); | ||
| receiveFunds(auctionType == AuctionType.NFT, auctionedToken, msg.sender, auctionedTokenIdOrAmount); | ||
| ) | ||
| external | ||
| whenNotPaused | ||
| nonEmptyString(name) | ||
| nonZeroAddress(auctionedToken) | ||
| nonZeroAddress(biddingToken) | ||
| { | ||
| require(duration > 0, "Duration must be > 0"); | ||
|
|
||
| receiveFunds( | ||
| auctionType == AuctionType.NFT, | ||
| auctionedToken, | ||
| msg.sender, | ||
| auctionedTokenIdOrAmount | ||
| ); | ||
|
|
||
| uint256 deadline = block.timestamp + duration; | ||
|
|
||
| auctions[auctionCounter] = AuctionData({ | ||
| id: auctionCounter, | ||
| name: name, | ||
|
|
@@ -82,45 +120,138 @@ contract AllPayAuction is Auction { | |
| availableFunds: 0, | ||
| minBidDelta: minBidDelta, | ||
| highestBid: 0, | ||
| winner: msg.sender, //Initially set to auctioneer,to ensure that auctioneer can withdraw funds in case of no bids | ||
| winner: msg.sender, | ||
| deadline: deadline, | ||
| deadlineExtension: deadlineExtension, | ||
| isClaimed: false, | ||
| protocolFee: protocolParameters.fee() | ||
| }); | ||
| emit AuctionCreated(auctionCounter++, name, description, imgUrl, msg.sender, auctionType, auctionedToken, auctionedTokenIdOrAmount, biddingToken, minimumBid, minBidDelta, deadline, deadlineExtension, protocolParameters.fee()); | ||
|
|
||
| emit AuctionCreated( | ||
| auctionCounter++, | ||
| name, | ||
| description, | ||
| imgUrl, | ||
| msg.sender, | ||
| auctionType, | ||
| auctionedToken, | ||
| auctionedTokenIdOrAmount, | ||
| biddingToken, | ||
| minimumBid, | ||
| minBidDelta, | ||
| deadline, | ||
| deadlineExtension, | ||
| protocolParameters.fee() | ||
| ); | ||
|
Comment on lines
+130
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
The post-increment Optional clarity refactor- emit AuctionCreated(
- auctionCounter++,
+ emit AuctionCreated(
+ auctionCounter,
...
);
+ auctionCounter++;🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| function bid(uint256 auctionId, uint256 bidIncrement) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) { | ||
| // ----------------------- | ||
| // BID | ||
| // ----------------------- | ||
|
|
||
| function bid(uint256 auctionId, uint256 bidIncrement) | ||
| external | ||
| whenNotPaused | ||
| exists(auctionId) | ||
| beforeDeadline(auctions[auctionId].deadline) | ||
| { | ||
| AuctionData storage auction = auctions[auctionId]; | ||
| require(auction.highestBid != 0 || bids[auctionId][msg.sender] + bidIncrement >= auction.minimumBid, 'First bid should be greater than starting bid'); | ||
| require(auction.highestBid == 0 || bids[auctionId][msg.sender] + bidIncrement >= auction.highestBid + auction.minBidDelta, 'Bid amount should exceed current bid by atleast minBidDelta'); | ||
|
|
||
| require( | ||
| auction.highestBid != 0 || | ||
| bids[auctionId][msg.sender] + bidIncrement >= auction.minimumBid, | ||
| "Bid too low" | ||
| ); | ||
|
|
||
| require( | ||
| auction.highestBid == 0 || | ||
| bids[auctionId][msg.sender] + bidIncrement >= | ||
| auction.highestBid + auction.minBidDelta, | ||
| "Bid below min increment" | ||
| ); | ||
|
|
||
| bids[auctionId][msg.sender] += bidIncrement; | ||
| auction.highestBid = bids[auctionId][msg.sender]; | ||
| auction.winner = msg.sender; | ||
| auction.availableFunds += bidIncrement; | ||
| auction.deadline += auction.deadlineExtension; | ||
|
|
||
| receiveERC20(auction.biddingToken, msg.sender, bidIncrement); | ||
|
|
||
| emit bidPlaced(auctionId, msg.sender, bids[auctionId][msg.sender]); | ||
| } | ||
|
|
||
| function withdraw(uint256 auctionId) external exists(auctionId) { | ||
| // ----------------------- | ||
| // WITHDRAW | ||
| // ----------------------- | ||
|
|
||
| function withdraw(uint256 auctionId) | ||
| external | ||
| whenNotPaused | ||
| exists(auctionId) | ||
| { | ||
| AuctionData storage auction = auctions[auctionId]; | ||
|
|
||
| uint256 withdrawAmount = auction.availableFunds; | ||
| auction.availableFunds = 0; | ||
| uint256 fees = (auction.protocolFee * withdrawAmount) / 10000; | ||
|
|
||
| uint256 fees = | ||
| (auction.protocolFee * withdrawAmount) / 10000; | ||
|
|
||
| address feeRecipient = protocolParameters.treasury(); | ||
| sendERC20(auction.biddingToken, auction.auctioneer, withdrawAmount - fees); | ||
| sendERC20(auction.biddingToken,feeRecipient,fees); | ||
|
|
||
| sendERC20( | ||
| auction.biddingToken, | ||
| auction.auctioneer, | ||
| withdrawAmount - fees | ||
| ); | ||
|
|
||
| sendERC20( | ||
| auction.biddingToken, | ||
| feeRecipient, | ||
| fees | ||
| ); | ||
|
|
||
| emit Withdrawn(auctionId, withdrawAmount); | ||
| } | ||
|
Comment on lines
+188
to
216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Anyone can call At minimum, add a deadline check. Consider restricting the caller as well. Proposed fix function withdraw(uint256 auctionId)
external
whenNotPaused
exists(auctionId)
+ onlyAfterDeadline(auctions[auctionId].deadline)
{As per coding guidelines: "Validate proper access control enforcement (e.g., Ownable, RBAC, role checks)." 🤖 Prompt for AI Agents |
||
|
|
||
| function claim(uint256 auctionId) external exists(auctionId) onlyAfterDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) { | ||
| // ----------------------- | ||
| // CLAIM (WITH ESCROW CHECK) | ||
| // ----------------------- | ||
|
|
||
| function claim(uint256 auctionId) | ||
| external | ||
| whenNotPaused | ||
| exists(auctionId) | ||
| onlyAfterDeadline(auctions[auctionId].deadline) | ||
| notClaimed(auctions[auctionId].isClaimed) | ||
| { | ||
| AuctionData storage auction = auctions[auctionId]; | ||
|
|
||
| // 🔐 ENSURE NFT ESCROW | ||
| if (auction.auctionType == AuctionType.NFT) { | ||
| require( | ||
| IERC721(auction.auctionedToken) | ||
| .ownerOf(auction.auctionedTokenIdOrAmount) == | ||
| address(this), | ||
| "NFT not escrowed" | ||
| ); | ||
| } | ||
|
|
||
| auction.isClaimed = true; | ||
| sendFunds(auction.auctionType == AuctionType.NFT, auction.auctionedToken, auction.winner, auction.auctionedTokenIdOrAmount); | ||
| emit Claimed(auctionId, auction.winner, auction.auctionedToken, auction.auctionedTokenIdOrAmount); | ||
| } | ||
| } | ||
|
|
||
| // TODO | ||
| sendFunds( | ||
| auction.auctionType == AuctionType.NFT, | ||
| auction.auctionedToken, | ||
| auction.winner, | ||
| auction.auctionedTokenIdOrAmount | ||
| ); | ||
|
|
||
| emit Claimed( | ||
| auctionId, | ||
| auction.winner, | ||
| auction.auctionedToken, | ||
| auction.auctionedTokenIdOrAmount | ||
| ); | ||
| } | ||
|
Comment on lines
+222
to
+256
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 🧩 Analysis chain🏁 Script executed: # Check ReentrancyGuard usage in AllPayAuction
rg -n "ReentrancyGuard" --type=sol contracts/Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 113 🏁 Script executed: # Find the AllPayAuction contract file and check its declaration
find . -name "AllPayAuction.sol" -type f | head -5Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 115 🏁 Script executed: # Check the contract inheritance and imports in AllPayAuction
head -50 contracts/AllPayAuction.solRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 1509 🏁 Script executed: # Find and examine the withdraw() function in AllPayAuction
rg -A 30 "function withdraw\(" contracts/AllPayAuction.solRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 826 🏁 Script executed: # Check VickreyAuction for ReentrancyGuard to confirm the reviewer's claim
head -50 contracts/VickreyAuction.solRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 2036 🏁 Script executed: # Check where sendFunds and sendERC20 are defined (likely in Auction.sol)
rg -n "function sendFunds" contracts/Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 217 🏁 Script executed: # Check where sendERC20 is defined
rg -n "function sendERC20" contracts/Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 201 🏁 Script executed: # Look at Auction abstract contract
cat contracts/abstract/Auction.solRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 3482 🏁 Script executed: # Check for other external calls in AllPayAuction that might have reentrancy issues
rg -n "IERC" contracts/AllPayAuction.sol | head -20Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 257 🏁 Script executed: # Check if there are other functions in AllPayAuction that might be vulnerable
rg -n "safeTransferFrom\|safeTransfer\|transfer\(" contracts/AllPayAuction.solRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 68 Consider adding The NFT escrow verification in That said, both functions follow the Checks-Effects-Interactions (CEI) pattern: 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; | |
| * @notice Auction contract for NFT and token auctions, where the highest bidder wins the auction and rest of the bidders get their bid refunded. | ||
| */ | ||
| contract EnglishAuction is Auction { | ||
| constructor (address _protocolParametersAddress) Auction(_protocolParametersAddress){} | ||
| constructor(address _protocolParametersAddress) Auction(_protocolParametersAddress) {} | ||
| mapping(uint256 => AuctionData) public auctions; | ||
| struct AuctionData { | ||
| uint256 id; | ||
|
|
@@ -87,7 +87,22 @@ contract EnglishAuction is Auction { | |
| isClaimed: false, | ||
| protocolFee: protocolParameters.fee() | ||
| }); | ||
| emit AuctionCreated(auctionCounter++, name, description, imgUrl, msg.sender, auctionType, auctionedToken, auctionedTokenIdOrAmount, biddingToken, minimumBid, minBidDelta, deadline, deadlineExtension, protocolParameters.fee()); | ||
| emit AuctionCreated( | ||
| auctionCounter++, | ||
| name, | ||
| description, | ||
| imgUrl, | ||
| msg.sender, | ||
| auctionType, | ||
| auctionedToken, | ||
| auctionedTokenIdOrAmount, | ||
| biddingToken, | ||
| minimumBid, | ||
| minBidDelta, | ||
| deadline, | ||
| deadlineExtension, | ||
| protocolParameters.fee() | ||
| ); | ||
|
Comment on lines
+90
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
Post-increment semantics are correct (emits the just-assigned ID), but this pattern is repeated across all 6 auction contracts. See the comment on 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| function bid(uint256 auctionId, uint256 bidAmount) external exists(auctionId) beforeDeadline(auctions[auctionId].deadline) { | ||
|
|
@@ -114,14 +129,22 @@ contract EnglishAuction is Auction { | |
| uint256 fees = (auction.protocolFee * withdrawAmount) / 10000; | ||
| address feeRecipient = protocolParameters.treasury(); | ||
| sendERC20(auction.biddingToken, auction.auctioneer, withdrawAmount - fees); | ||
| sendERC20(auction.biddingToken,feeRecipient,fees); | ||
| sendERC20(auction.biddingToken, feeRecipient, fees); | ||
| emit Withdrawn(auctionId, withdrawAmount); | ||
| } | ||
|
|
||
| function claim(uint256 auctionId) external exists(auctionId) onlyAfterDeadline(auctions[auctionId].deadline) notClaimed(auctions[auctionId].isClaimed) { | ||
| AuctionData storage auction = auctions[auctionId]; | ||
|
|
||
| // Verify NFT is properly escrowed | ||
| if (auction.auctionType == AuctionType.NFT) { | ||
| require(IERC721(auction.auctionedToken).ownerOf(auction.auctionedTokenIdOrAmount) == address(this), 'NFT not escrowed'); | ||
| } | ||
|
|
||
| auction.isClaimed = true; | ||
|
|
||
| sendFunds(auction.auctionType == AuctionType.NFT, auction.auctionedToken, auction.winner, auction.auctionedTokenIdOrAmount); | ||
|
|
||
| emit Claimed(auctionId, auction.winner, auction.auctionedToken, auction.auctionedTokenIdOrAmount); | ||
| } | ||
|
Comment on lines
136
to
149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find the Auction abstract contract and examine sendFunds
fd -t f "\.sol$" | xargs grep -l "function sendFunds"Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 116 🏁 Script executed: # Check if ReentrancyGuard is imported or used in EnglishAuction
fd -t f "EnglishAuction\.sol" | xargs grep -E "(ReentrancyGuard|import)"Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 335 🏁 Script executed: # Examine the complete EnglishAuction contract structure
fd -t f "EnglishAuction\.sol" | head -1 | xargs cat -nRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 6989 🏁 Script executed: # Check withdraw() function and bid() function in EnglishAuction
fd -t f "EnglishAuction\.sol" | xargs grep -A 15 "function withdraw\|function bid"Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 1992 🏁 Script executed: # Read the Auction abstract contract
fd -t f "Auction\.sol" -path "*/abstract/*" | head -1 | xargs cat -nRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 254 🏁 Script executed: # Specifically search for sendFunds and receiveERC20, sendERC20 implementations
fd -t f "Auction\.sol" -path "*/abstract/*" | xargs grep -A 10 "function sendFunds\|function receiveERC20\|function sendERC20"Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 256 🏁 Script executed: # Find and read Auction.sol
find . -name "Auction.sol" -type f | grep abstract | head -1 | xargs cat -nRepository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 4168 🏁 Script executed: # Search for function implementations in the abstract Auction
find . -name "Auction.sol" -type f | xargs grep -A 12 "function sendFunds\|function receiveERC20\|function sendERC20"Repository: StabilityNexus/HammerAuctionHouse-Solidity Length of output: 1347 NFT escrow verification in The check at lines 140-142 properly verifies contract ownership of the NFT before transfer, and However, the contract lacks 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pausable/Ownable added here but missing from EnglishAuction, VickreyAuction, LinearReverseDutchAuction, and LogarithmicReverseDutchAuction.
Only
AllPayAuctionandExponentialReverseDutchAuctiongetOwnable + Pausablein this PR. The other four auction contracts have no pause mechanism. This inconsistency means an emergency pause would only partially halt the protocol. If pausing is deemed necessary, it should be applied uniformly. Additionally, this change is unrelated to the NFT escrow fix and arguably should be in a separate PR.As per coding guidelines: "Flag pull requests that mix unrelated changes or multiple concerns in a single submission."
Also applies to: 15-20, 67-73
🤖 Prompt for AI Agents