Starknet Auction

First Flight #26
Beginner FriendlyNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Owner is unable to get back the NFT if no bids were placed by the end of the auction period

Summary

The NFT owner cannot retrieve back the NFT ownership if no bids are placed by the end of the auction period. The owner will be unable to execute the end function as it will revert with a No bids error message, leaving the NFT gets stuck in the contract permanently.

https://github.com/SiewLin-AIDeaBuddy/first-flight-2024-10-starknet-auction/blob/04f8a8b7f16edc7aeba06a3a6195e0257ddbefd0/src/starknet_auction.cairo#L149

Vulnerability Details

When the auction period ends, owner can call end function to transfer the NFT to the highest bidder and receive the tokens bid amount from the highest bidder via withdraw function. However, if there's no bids were placed by the end of the auction period, owner won't be able to retrieve back the NFT from the contract due to the assertion check assert(self.starting_price.read() < self.highest_bid.read(), 'No bids'); in the end function.

fn end(ref self: ContractState) {
let time = get_block_timestamp();
let caller = get_caller_address();
let erc721_dispatcher = IERC721Dispatcher { contract_address: self.erc721_token.read() };
let sender = get_contract_address();
assert(caller == self.nft_owner.read(), 'Not the nft owner');
assert(self.started.read(), 'Auction is not started');
assert(time >= self.bidding_end.read(), 'Auction is not yet ended');
assert(!self.ended.read(), 'Auction end is already called');
<@@>! assert(self.starting_price.read() < self.highest_bid.read(), 'No bids');
self.ended.write(true);
self.emit(End {highest_bid: self.highest_bid.read(), highest_bidder: self.highest_bidder.read()});
erc721_dispatcher.transfer_from(sender, self.highest_bidder.read(), self.nft_id.read().into());
}

When no bids were placed, the self.starting_price will be the same as self.highest_bid, and therefore causes the assertion at assert(self.starting_price.read() < self.highest_bid.read(), 'No bids'); to fail with No bids error. As the owner can't complete the end function and there's no other way for the owner to retrive back the NFT when no bids were placed, owner's NFT will therefore get stuck in the contract permanently.

Proof of Concept:

Step 1: In test\test_contract.cairo, add the following test case:

#[test]
#[should_panic(expected: 'No bids')]
fn test_audit_owner_cant_get_back_nft() {
let (auction_dispatcher, auction_contract, _, erc721_contract_address) = deploy_auction_contract();
let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_contract_address };
let time = get_block_timestamp();
// The owner calls start function.
auction_dispatcher.start(86400, 10);
start_cheat_block_timestamp(auction_contract, time + 86401);
auction_dispatcher.end();
stop_cheat_block_timestamp(auction_contract);
}

Step 2: Run snforge test --features enable_for_tests test_audit_owner_cant_get_back_nft

...
Collected 1 test(s) from starknet_auction package
Running 1 test(s) from tests/
[PASS] starknet_auction_integrationtest::test_contract::test_audit_owner_cant_get_back_nft (gas: ~1680)
Running 0 test(s) from src/
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 17 filtered out

The test passes with expected panic of No bids via #[should_panic(expected: 'No bids')]

Impact

Owner can't retrive back the NFT when no bids are placed by the end of the auction period.

Tools Used

Manual review

Recommendations

To make amendment on the assertion check as follow so owner will still be able to get back the NFT in the event that no bids are placed by end of auction period

fn end(ref self: ContractState) {
let time = get_block_timestamp();
let caller = get_caller_address();
let erc721_dispatcher = IERC721Dispatcher { contract_address: self.erc721_token.read() };
let sender = get_contract_address();
assert(caller == self.nft_owner.read(), 'Not the nft owner');
assert(self.started.read(), 'Auction is not started');
assert(time >= self.bidding_end.read(), 'Auction is not yet ended');
assert(!self.ended.read(), 'Auction end is already called');
- assert(self.starting_price.read() < self.highest_bid.read(), 'No bids');
+ if (self.starting_price.read() < self.highest_bid.read()) {
+ assert(self.starting_price.read() < self.highest_bid.read(), 'No bids');
+ }
self.ended.write(true);
self.emit(End {highest_bid: self.highest_bid.read(), highest_bidder: self.highest_bidder.read()});
erc721_dispatcher.transfer_from(sender, self.highest_bidder.read(), self.nft_id.read().into());
}

Modify earlier test case to verify that the contract doesn't hold the NFT with the amendment suggested above when owner calls the end function even when there's no bids were placed by the end of the auction period.

#[test]
fn test_audit_owner_cant_get_back_nft() {
let (auction_dispatcher, auction_contract, _, erc721_contract_address) = deploy_auction_contract();
let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_contract_address };
let time = get_block_timestamp();
// The owner calls start function.
auction_dispatcher.start(86400, 10);
start_cheat_block_timestamp(auction_contract, time + 86401);
start_cheat_caller_address_global(auction_contract);
erc721_dispatcher.approve(auction_contract, 1);
stop_cheat_caller_address_global();
// before owner calls `end` function
let nft_balance_before_end = erc721_dispatcher.balance_of(auction_contract);
assert(nft_balance_before_end == 1, 'Nft balance must be 1');
auction_dispatcher.end();
// after owner calls `end` function
let nft_balance_after_end = erc721_dispatcher.balance_of(auction_contract);
assert(nft_balance_after_end == 0, 'Nft balance must be 0');
stop_cheat_block_timestamp(auction_contract);
}

Run the test snforge test --features enable_for_tests test_audit_owner_cant_get_back_nft and it will pass

....
Collected 1 test(s) from starknet_auction package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] starknet_auction_integrationtest::test_contract::test_audit_owner_cant_get_back_nft (gas: ~1769)
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 17 filtered out
Updates

Lead Judging Commences

bube Lead Judge 8 months ago
Submission Judgement Published
Validated
Assigned finding tags:

The NFT will be locked if there are no bids

If there are no placed bids in the auction, the `end` function will always revert. The owner can not receive back the nft ant it will be locked in the contract.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.