This article continues on the series we started the last time: Solidity smart contract examples, which implement a simplified real-world process.
Here, we’re walking through an example of a simple open auction.
π Original Source Code: Solidity Docs
We’ll first lay out the entire smart contract example without the comments for readability and development purposes.
Then we’ll dissect it part by part, analyze it and explain it.
Following this path, we’ll get a hands-on experience with smart contracts, as well as good practices in coding, understanding, and debugging smart contracts.
Smart contract – Simple Open Auction
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; contract SimpleAuction { address payable public beneficiary; uint public auctionEndTime; address public highestBidder; uint public highestBid; mapping(address => uint) pendingReturns; bool ended; event HighestBidIncreased(address bidder, uint amount); event AuctionEnded(address winner, uint amount); error AuctionAlreadyEnded(); error BidNotHighEnough(uint highestBid); error AuctionNotYetEnded(uint timeToAuctionEnd); error AuctionEndAlreadyCalled(); constructor( uint biddingTime, address payable beneficiaryAddress ) { beneficiary = beneficiaryAddress; auctionEndTime = block.timestamp + biddingTime; } function bid() external payable { if (block.timestamp > auctionEndTime) revert AuctionAlreadyEnded(); if (msg.value <= highestBid) revert BidNotHighEnough(highestBid); if (highestBid != 0) { pendingReturns[highestBidder] += highestBid; } highestBidder = msg.sender; highestBid = msg.value; emit HighestBidIncreased(msg.sender, msg.value); } function withdraw() external returns (bool) { uint amount = pendingReturns[msg.sender]; if (amount > 0) { pendingReturns[msg.sender] = 0; if (!payable(msg.sender).send(amount)) { pendingReturns[msg.sender] = amount; return false; } } return true; } function auctionEnd() external { if (block.timestamp < auctionEndTime) revert AuctionNotYetEnded(auctionEndTime - block.timestamp); if (ended) revert AuctionEndAlreadyCalled(); ended = true; emit AuctionEnded(highestBidder, highestBid); beneficiary.transfer(highestBid); } }
Code breakdown and analysis
// SPDX-License-Identifier: GPL-3.0
Compiles only with Solidity compiler version 0.8.4 and later, but before version 0.9.
π Learn More: Layout of a Solidity File
pragma solidity ^0.8.4; contract SimpleAuction {
Parameters of the auction are variables beneficiary
and auctionEndTime
which we’ll initialize with contract creation arguments while the contract gets created, i.e. in the contract constructor.
Data type for time variables is unsigned integer uint
, so that we can represent either absolute Unix timestamps (seconds since 1970-01-01) or time periods in seconds (seconds lapsed from the reference moment we chose).
address payable public beneficiary; uint public auctionEndTime;
The current state of the auction is reflected in two variables, highestBidder
and highestBid
.
address public highestBidder; uint public highestBid;
Previous bids can be withdrawn, that’s why we have mapping data structure to record pendingReturns
.
mapping(address => uint) pendingReturns;
Indicator flag variable for the auction end. By default, the flag is initialized to false
; we’ll prevent changing it once it switches to true
.
bool ended;
When changes occur, we want our smart contract to emit the corresponding change events.
event HighestBidIncreased(address bidder, uint amount); event AuctionEnded(address winner, uint amount);
We’re defining four errors to describe relevant failures. Along with these errors, we’ll also introduce “triple-slash” comments, commonly known as natspec
comments. They enable users to see comments when an error is displayed or when users are asked to confirm the transaction.
π Learn More: Natspec comments are formally defined in Ethereum Natural Language Specification Format.
/// The auction has already ended. error AuctionAlreadyEnded(); /// There is already a higher or equal bid. error BidNotHighEnough(uint highestBid); /// The auction has not ended yet, the remaining seconds are displayed. error AuctionNotYetEnded(uint timeToAuctionEnd); /// The function auctionEnd has already been called. error AuctionEndAlreadyCalled();
Initialization of the contract with the contract creation arguments biddingTime
and beneficiaryAddress
.
/// Create a simple auction with `biddingTime` /// seconds bidding time on behalf of the /// beneficiary address `beneficiaryAddress`. constructor( uint biddingTime, address payable beneficiaryAddress ) { beneficiary = beneficiaryAddress; auctionEndTime = block.timestamp + biddingTime; }
A bidder bids by sending the currency (paying) to the smart contract representing the beneficiary, hence the bid()
function is defined as payable
.
π Learn More: What is payable
in Solidity?
/// Bid on the auction with the value sent /// together with this transaction. /// The value will only be refunded if the /// auction is not won. function bid() external payable {
The function call reverts if the bidding period ended.
if (block.timestamp > auctionEndTime) revert AuctionAlreadyEnded();
The function rolls back the transaction to the bidder if the bid does not exceed the highest one.
if (msg.value <= highestBid) revert BidNotHighEnough(highestBid);
The previous highest bidder was outbid and his bid is added to his previous bids reserved for a refund.
π‘ A direct refund is considered a security risk due to the possibility of executing an untrusted contract.
Instead, the bidders (recipients) will withdraw their bids themselves by using withdraw() function below.
if (highestBid != 0) { pendingReturns[highestBidder] += highestBid; }
The new highest bidder and his bid are recorded; the event HighestBidIncreased
is emitted carrying this information pair.
highestBidder = msg.sender; highestBid = msg.value; emit HighestBidIncreased(msg.sender, msg.value); }
Bidders call the withdraw()
function to retrieve the amount they bid.
/// Withdraw a bid that was overbid. function withdraw() external returns (bool) { uint amount = pendingReturns[msg.sender]; if (amount > 0) {
It is possible to call the withdraw()
function again before the send()
function returns. That’s the reason why we need to disable multiple sequential withdrawals from the same sender by setting the pending returns for a sender to 0.
pendingReturns[msg.sender] = 0;
Variable type of msg.sender
is not address payable
, therefore we need to convert it explicitly by using function payable() as a wrapping function.
If the send()
function ends with an error, we’ll just reset the pending amount and return false
.
if (!payable(msg.sender).send(amount)) { // No need to call throw here, just reset the amount owing pendingReturns[msg.sender] = amount; return false; } } return true; }
The auctionEnd()
function ends the auction and sends the highest bid to the beneficiary.
The official Solidity documentation recommends dividing the interacting functions into three functional parts:
- checking the conditions,
- performing the actions, and
- interacting with other contracts.
Otherwise, by combining these parts rather than keeping them separated, more than one calling contract could try and modify the state of the called contract and change the called contract’s state.
/// End the auction and send the highest bid /// to the beneficiary. function auctionEnd() external {
Checking the conditions…
if (block.timestamp < auctionEndTime) revert AuctionNotYetEnded(auctionEndTime - block.timestamp); if (ended) revert AuctionEndAlreadyCalled();
…performing the actions…
ended = true; emit AuctionEnded(highestBidder, highestBid);
…and interacting with other contracts.
beneficiary.transfer(highestBid); } }
Our smart contract example is a simple, but a powerful one, enabling us to bid an amount of currency to the beneficiary.
When the contract instantiates via its constructor, it sets the auction end time and its beneficiary, i.e. beneficiary address.
The contract has three simple features, implemented via dedicated functions: bidding, withdrawing the bids and ending the auction.
A new bid is accepted only if its amount is strictly larger than the current highest bid. A new bid acceptance means that the current highest bid is added to the bidder’s balance for later withdrawal. The new highest bidder becomes the current highest bidder and the new highest bid becomes the current highest bid.
Bid withdrawing returns all summed previous bids to each bidder (mapping pendingReturns
).
Contract Test Scenario
Open auction duration (in seconds): 240
Beneficiary: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
Testing/demonstration steps:
Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 bids 10 Wei;
Account 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db bids 25 Wei;
Account 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB bids 25 Wei (rejected);
Account 0x617F2E2fD72FD9D5503197092aC168c91465E7f2 bids 35 Wei;
Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 bids 40 Wei + initiates premature auction end;
Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 withdraws his bids;
Account 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db withdraws his bids;
Account 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB withdraws his bids;
Account 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB initiates timely auction end;
Account 0x617F2E2fD72FD9D5503197092aC168c91465E7f2 withdraws his bids;
Appendix – The Contract Arguments
In this section is additional information for running the contract. We should expect that our example accounts may change with each refresh/reload of Remix.
Our contract creation arguments are the open auction duration (in seconds) and the beneficiary address (copy this line when deploying the example):
300, 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
π‘ Info: we could’ve used any amount of time, but I went with 300 seconds to timely simulate both a rejected attempt of ending the auction and the successful ending of the auction.
Conclusion
We continued our smart contract example series with this article that implements a simple open auction.
First, we laid out clean source code (without any comments) for readability purposes. Omitting the comments is not recommended, but we love living on the edge – and trying to be funny! π
Second, we dissected the code, analyzed it, and explained each possibly non-trivial segment. Just because we’re terrific, safe players who never risk it and do everything by the book π