Decentralized NFT Auction

Go up to the CCC HW page (md) | view one-page version

Overview

In this assignment you will write a smart contract, in Solidity, to handle auctions for NFTs. The NFTs will be ERC-721 tokens.

Once deployed to our private Ethereum blockchain, anybody should be able to mint an NFT and then initiate an auction. Anybody could then submit a bid to the auction. To prevent somebody from placing a bid and then not paying, one has to transfer ETH to the smart contract when a bid is placed – it is the transfer of this ETH (along with the associated function call) that actually places the bid. Anybody who is outbid will have their ETH returned, and they can choose (or not) to place a higher bid. Once the auction is completed, the ETH from the winning bid is transferred to the person who initiated the auction (minus some fees), and the NFT is transferred to the winning bidder.

Writing this homework will require completion of the following assignments:

The intent is that you are going to re-use the three NFT images that you created in the Tokens assignment. You can also create new images, if you would like, as long as you follow the guidelines in that assignment (public domain, nothing that will get me in trouble, file naming, in the ipfs/ directory, etc.). As before, in this course, owning the NFT does NOT imply ownership of the image – the assumption is that you don’t actually own the original image, since it’s in the public domain.

You will also need to be familiar with the Ethereum slide set, the Solidity slide set, and the Tokens slide set

In addition to your source code, you will submit an edited version of auction.py (src).

Changelog

Any changes to this page will be put here for easy reference. Typo fixes and minor clarifications are not listed here. So far there aren’t any significant changes to report.

Task 1: Auction contract

You are going to create and deploy a decentralized auction smart contract. The contract you will be creating will allow for a decentralized auction for NFTs.

This section is meant as a high-level overview of the process; the detailed specifications are in the next two sections.

Task 2: IAuctioneer interface

This task is to understand the IAuctioneer interface. Formally the task is to develop an Auctioneer contract that implements the following IAuctioneer interface below. The provided IAuctioneer.sol (src) file has more comments for this interface. There is a lot that some of these funcctions have to do, and that is specified in the comments in the IAuctioneer.sol file.

Your contract line must be exactly:

contract Auctioneer is IAuctioneer {

The interface is as follows. There are much more detailed comments in the IAuctioneer.sol (src) file.

// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import "./IERC165.sol";

interface IAuctioneer is IERC165 {

    // Holds the information for each auction
    struct Auction {
        uint id;            // the auction id
        uint num_bids;      // how many bids have been placed
        string data;        // a text description of the auction or NFT data
        uint highestBid;    // the current highest bid, in wei
        address winner;     // the current highest bidder
        address initiator;  // who started the auction
        uint nftid;         // the NFT token ID
        uint endTime;       // when the auction started
        bool active;        // if the auction is active
    }


    // there needs to be a constructor, but those are never listed in an interface


    // the following are just the getter methods for the public variables in the contract

    function nftmanager() external view returns (address);

    function numAuctions() external view returns (uint);

    function totalFees() external view returns (uint);

    function uncollectedFees() external view returns (uint);

    function auctions(uint id) external view 
            returns (uint, uint, string memory, uint, address, address, uint, uint, bool);

    function deployer() external view returns (address);    


    // The following are functions you must create

    function collectFees() external;

    function startAuction(uint m, uint h, uint d, string memory data, 
                          uint reserve, uint nftid) external returns (uint);

    function closeAuction(uint id) external;

    function placeBid(uint id) payable external;

    function auctionTimeLeft(uint id) external view returns (uint);


    // the three events that needs to be emitted at the appropriate times

    event auctionStartEvent(uint indexed id);

    event auctionCloseEvent(uint indexed id);

    event higherBidEvent (uint indexed id);


    // also supportsInterface(), because IAuctioneer inherits from IERC165
}

This interface is provided in the IAuctioneer.sol (src) file. This interface extends the IERC165.sol (src) interface, which requires the implementation of a supportsInterface() function – your Auctioneer class thus supports two interfaces (IAuctioneer and IERC165).

For a contract to transfer ETH to another account, you can use code such as the following; this was also discussed in the Solidity slide set. Note that the address to pay to is in variable a, and the value – in wei – is in v:

(bool success, ) = payable(a).call{value: v}("");
require(success, "Failed to transfer ETH");

As you are testing it, you will notice in Remix that the button for placeBid() is red – that is because this is a payable function. When you call this function, after setting the correct auction ID as the parameter, you will need to transfer some ETH along with the call. In the deployment pane in Remix, just enter a numerical value in the ‘Value’ box, and select the right denomination (wei, gwei, ether, etc.). That amount of ETH will be transferred along with the function call. If the call reverts, then you get that money back (minus the gas fees, if it tried to send the transaction to the blockchain). If you have a mistake in your function code, you will likely lose that ETH – this is why we are testing this on the Javascript deployment environment in Remix and then on a private blockchain where the ETH has no value.

Test all this thoroughly in Remix! You will need to deploy your Auctioneer contract in Remix’s Javascript environment to test everything working together. Recall that you have to select the right contract to deploy in the “Contract” list, else Remix may not know which one to deploy. Be sure to develop via incremental development, else you will not be able to figure out where your bug is.

One it works, deploy it to our private Ethereum blockchain. You should test it there as well. You will need to submit the contract address of the deployed Auctioneer. If you deploy it multiple times, just submit the most recent contract address. Once it is deployed to our private Ethereum blockchain, you can view it on the auctions page, the URL of which is on the Canvas landing page; a link to this will also be shown on the explorer page for your Auctioneer contract. This auctions web page will make it far easier to see what is going on with your auctions. Note that the explorer will only display this link if it knows that the contract implements IAuctioneer, and it only knows that if your supportsInterface() method is written and correct.

totalFees() versus uncollectedFees()

Fees accumulate during the life of the auction contract – 1% of successful auctions is saved as fees. The deployer of the contract can then obtain all the fees collected so far by calling collectFees(). All amounts return values in wei.

totalFees() is the total amount of fees that have been collected over the life of the auction contract. uncollectedFees() is the amount that can currently be paid out.

As an example, imagine there are two successful auctions that accumulate a total of 2 ether in fees. Both totalFees() and uncollectedFees() will return 2 ether (really 2 * 1018 wei). collectFees() is called, and the 2 ether is paid to the deployer (minus gas fees, of course). Now totalFees() still reports 2 ether, since that is how much has been accumulated over the life of the contract. But uncollectedFees() will return 0, since there are no more fees that can be paid to the deployer. If more auctions accumulate 1 ether in additional fees, then totalFees() will return 3 ether (really 3 * 1018 wei) and uncollectedFees() will return 1 ether (really 1 * 1018 wei).

startAuction() method

The startAuction() method requires a bit more explanation. The process is as follows:

Below is a diagram of the flow of this process.

Task 3: Create auctions

You should create two auctions in your Auctioneer contract (you’ll create a third one below as well). It’s fine if you create more (such as from testing) – we will only look at the two requested here. These two auctions will use two of your three NFT images. In particular, if you have one NFT that you like more than the others, or is “better”, you will want to save it for the course-wide auction, below.

Note that you can perform these calls through Remix (via calling an external contract, as described in the dApp introduction (md) assignment) or through geth calls (as described in the Solidity slide set).

If you screw up one of these auctions, you can always just create more. We don’t care how many auctions you have created on your contract, as long as the two that are required below fulfill those requirements.

Auction 1

The first one should be an auction that has fully ended by due date/time of the assignment. Basically, we want it to be a closed auction. There should be a few bids on this auction. You can create multiple accounts for this – just call personal.newAccount() a few more times – each account is in the eth.accounts list, and you will have to unlock each one with personal.unlockAccount(). To get ether into those other accounts you can:

You can also get classmates to bid on your auction, although that is not required. This auction will use the first of your (three) NFTs. You will be submitting the auction ID for this auction as well as the NFT token ID.

You SHOULD call closeAuction() on this auction.

Auction 2

The second auction should end one week after the assignment is due. Just get it on the day one week later – we don’t really care about the time, as long as the date is 7 days after the assignment due date. Basically, we want to see an active auction. This, also, should have a few bids on it. This auction use the second of your (three) NFTs. You will be submitting the auction ID for this auction as well as the NFT token ID.

This auction should have a reserve of 1 ether. Keep in mind that you have to enter this in wei in the Remix function call box, which means a reserve value of 1 ether is entered as 1000000000000000000 (wei).

View your auctions

There is a web page to view your auctions, and the URL for it is on the Canvas landing page. You can also get a link to it from the explorer page for your deployed smart contract. This can be used to view any auction smart contract that implements the IAuctioneer interface. This means you can view the class auctions as well (which are done in the next section).

Task 4: Class Auctions

You are going to participate in a class-wide auction manager.

We have deployed an auction manager, and the contract address for that Auctioneer contract is on the Canvas landing page. As above, you can perform these calls through Remix (via calling an external contract, as described in the dApp introduction (md) assignment) or through geth calls (as described in the Solidity slide set).

You should use one of your three NFTs that you didn’t use in the previous section. You should create an auction that ends one week after the due date of the assignment (again, we are looking for the day – we don’t care too much about the time of day). You will need to submit the auction ID from the auction you created as well as the NFT token ID. YOUR RESERVE should be no higher than 5 ETH.

Lastly, bid on at least three auctions that are not your own. Depending on when you submit your assignment, there may not be any (or any interesting) auctions available to bid on. That’s fine – you don’t have to have those bids completed by the time the assignment is due; you have an extra few days to place your bids. We are going to judge lateness on this assignment by the Gradescope submission time, and the information you have to submit does not include the transaction hashes of the bids. We are going to check whether you bid on the auctions by looking if your eth.coinbase account, the address of which you will submit below, initiated bids on any one of your classmate’s submitted NFT manager addresses by two days after the due date. Note that you have to place the bid via Remix or geth; the course website just displays the auctions.

MAKE YOUR BIDS REASONABLE!!! If the current highest bid is 0.5 ETH, don’t suddenly bid 5,000 ETH. Doing so is going to require others who need to bid on that NFT to have to obtain a lot more ETH, which will increase the blockchain size and the difficulty, which will make it harder for everybody else in the class. This will make me very cranky. Any successive bid should be no more than about 1 ETH more than the previous bid.

Notes and Hints

Paying the null address

An easy way to implement the reserve is to set the highestBid field to the reserve, and the winner to the null address (meaning: address(0)). That’s fine, but be sure to put in a check if the winning bidder is the null address. If, on a higher bid, you pay the previously highest bidder, and that is the null address, you will forever lose that ether. While we don’t really care about losing ether on our blockchain, this will deplete the balance of the Auctioneer contract, which means it will not be able to pay out the winners of the successful auctions. This will cause your contract to not work properly.

Return values from transactions

In Remix, any call – meaning a view or pure function, which shows up in Remix as a blue button – will display the return value underneath that button when called. But Remix has a harder time determining the return value of transactions (orange buttons), which also include payable functions. Sometimes Remix can determine the return value, and it is in the JSON data shown in the console window (the window below where you edit the code). Other times, Remix cannot determine the return value of transactions. But the explorer can – so if you are expecting a return value, and Remix does not display it, view the transaction in the explorer, and that will have the return value. This is the case when trying to find the NFT ID of a newly minted NFT.

block.timestamp behavior

block.timestamp behaves differently on the Javascript blockchain in Remix and on the course blockchain. Consider the following contract:

// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.24;
contract BlockTimestamp {
    function getTimestamp() public view returns (uint) {
        return block.timestamp;
    }
}

On the Javascript blockchain in Remix, this will always return the current time as per the system clock; this means it will give a different value every second. On the course blockchain, it will always return the time of the last block. If you click the button and then wait 10 seconds, the first one (Javascript) will show a new timestamp, whereas the second one (course blockchain) will not show any update unless a new block was added to the blockchain in that time.

This has implications for calling closeAuction(). Let’s assume you have some code therein such as: require (auctions[id].endTime < block.timestamp, "...");. This will revert if the current time is not yet after the auction end time. So let’s assume that the current time is after the auction end time. In the Javascript environment, this will work fine, since block.timestamp is always the current system time. If you are trying to call this on the course blockchain, Remix will try to guess block.timestamp based on the last block (not the block it is about to be created for the transaction). If there are no other blocks on the blockchain that were added after the auction end time, Remix will predict that this transaction will revert, since it thinks that the require condition will evaluate to false based on it’s estimate of block.timestamp from the last block. What will really happen is that the transaction will be put into a new block, whose timestamp will be (we are assuming) after the auction end time, and the require() will pass this test.

Putting in the require() shown above is completely acceptable. This section is just explaining the difference in behavior that you will see, and that Remix will state that it is going to revert when, in this one case, it will not.

Submission

You will need to fill in the various values from this assignment into the auction.py (src) file. That file clearly indicates all the values that need to be filled in. That file, along with your Solidity source code, are the only files that must be submitted. The sanity_checks dictionary is intended to be a checklist to ensure that you perform the various other requirements to ensure this assignment is fully submitted.

There are three forms of submission for this assignment; you must do all three.

Submission 1: You must deploy you Auctioneer smart contract (which will deploy its own NFTmanager contract) to our private Ethereum blockchain. It’s fine if you deploy it a few times to test it. But the final deployment for the Auctioneer should only have the auctions specified in task 3, above. Save the contract addresses of that deployment, as it will go in the auction.py file that you submit below.

Submission 2: You have to create a number of auctions: two in your auction manager, and one in the course-wide auction manager. These have specific close dates, and there should be multiple bids on the first two. This is detailed in tasks 3 and 4, above.

Submission 3: You should submit your Auctioneer.sol file and your completed auction.py file to Gradescope. You will also need to submit your NFTManager.sol file from the last assignment. All your Solidity code for this assignment should be in that first file, and you should specifically import the various interfaces. Those interface files will be placed in the same directory on Gradescope when you submit. NOTE: Gradescope cannot fully test this assignment, as it does not have access to the private blockchain. So it can only do a few sanity tests (correct files submitted, successful compilation, valid values in auction.py, etc.).