Black friday

Save big!

All courses under $5 - for a limited time!

Code has been added to clipboard!

From Voting to Auctions: Learn With Solidity Examples

Reading time 11 min
Published Jul 5, 2019
Updated Nov 6, 2019

In the previous chapters, we covered the fundamentals of Solidity language theory. Now, we will put them to practice.

In the following sections, we will review five Solidity examples step by step. First, we will analyze a smart contract written to create a simple Solidity subcurrency. Then, we will explain how blockchain voting and auctions - both simple and blind - work in Ethereum, and how you can make a safe remote sale using Solidity.

If you want to try yourself in writing your own smart contracts, you should also look into this interactive course.

Solidity Examples: Main Tips

  • When you create a Solidity subcurrency, anyone can send coins, but only the contract creator can issue new ones.
  • A blockchain voting system ensures a transparent process where the chairperson assigns voting rights to each address individually, and the votes are counted automatically.
  • Using smart contracts, you can create simple open auctions, as well as blind ones.
  • A Solidity contract can act as an agreement between a buyer and a seller when selling an item remotely.

Ethereum Subcurrency Standard

The code we will analyze now creates a basic form of cryptocurrency. Anyone who has an Ethereum keypair can exchange these coins. However, only the contract creator can issue new ones.

First of all, we include the version pragma to avoid compatibility issues:

Example
pragma solidity >=0.5.0 <0.7.0;

Now we can start with the contract itself. The first state variable we have to declare is address. It has a 160-bit value and stores the address of your smart contract. By entering public, we let other contracts access the variables we declared.

mapping will map the address to uint variables (unsigned integers):

Example
contract Coin {
    address public minter;
    mapping (address => uint) public balances;

The next line declares an event. It will be triggered when the send function executes. By listening to these events and receiving from, to, and amount arguments, Ethereum clients can track transactions.

Example
event Sent(address from, address to, uint amount);

The constructor will execute once when you create a contract:

Example
constructor() public {
        minter = msg.sender;
    }

The contract contains two functions: mint and send. Only you, as the contract creator, can call mint. By doing that, you create and send a specified amount of coins to another address.

To define the conditions in which the changes should be reverted, you should use a require function call. In the code snippet below, it makes sure the minter is actually the contract creator and defines the maximum amount of coins to send:

Example
function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        require(amount < 1e50);
        balances[receiver] += amount;
    }

Unlike mint, send is available to use for anyone who possesses coins. By executing it, they can send an amount of coins to someone else.

If the sender tries to send more coins than they actually own, the require function call won’t execute. In this case, an error message will pop up:

Example
function send(address receiver, uint amount) public {
        require(amount <= balances[msg.sender], "Balance too low!");
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

Creating a Blockchain Voting System

While a lot of people tend to associate smart contracts with financial matters, decentralized apps can be used in many more ways. With our next example, we will illustrate using blockchain for voting electronically.

The most important things to ensure in online voting are:

  • Correct assignment of the voting rights
  • Automatic vote counting
  • Transparent process

To do all that, you will need to create one contract for each vote and name every available option.

After the contract creator provides voting rights to each address, their owner can use or delegate their votes. When our blockchain election finishes, the proposal that received the most votes is returned.

As usual, we declare the version pragma before starting our contract:

Example
pragma solidity >=0.4.22 <0.7.0;

contract Ballot {

Now we have to write the struct to represent a single vote holder. It holds the information whether the person voted, and (if they did) which option they chose.

If they trusted someone else with their ballot, you can see the new voter in the address line. Delegation also makes weight accumulate:

Example
struct Voter {
        uint weight;
        boolean if_voted;
        address delegated_to;
        uint vote;
    }

This next struct will represent a single proposal. Its name can’t be bigger than 32 bytes. The voteCount shows you how many votes the proposal has received:

Example
struct Proposal {
        bytes32 name;
        uint voteCount;
    }

    address public chairperson;

By using the mapping keyword, we declare a state variable necessary for assigning the blockchain voting rights. It stores the struct we created to represent the voter in each address required:

Example
mapping(address => Voter) public voters;

Next, we include a proposal structs array. It will be dynamically-sized:

Example
Proposal[] public proposals;

Then, we create a new ballot to choose one of the proposals. Each new proposal name requires creating a new proposal object and placing it at the end of our array:

Example
constructor(bytes32[] memory proposalNames) public {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        for (uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

Now it’s time to provide the voter with a right to use their ballot. Only the chairperson can call this function.

If the first argument of require returns false, function stops executing. All the changes it has made are cancelled too.

Using require helps you check if the function calls execute as they should. If they don’t, you may include an explanation in the second argument:

Example
function giveRightToVote(address voter) public {
        require(
            msg.sender == chairperson,
            "Only the chairperson can assign voting rights."
        );
        require(
            !voters[voter].voted,
            "The voter has used their ballot."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

Next, we will write a function to delegate the vote. to represents an address to which the voting right goes. If they delegate as well, the delegation is forwarded.

We also add a message to inform about a located loop. Those can cause issues if they run for a long time. In such case, a contract might get stuck, as delegation may need more gas than a block has available:

Example
function delegate(address to) public {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "You have already voted.");

        require(to != msg.sender, "You can’t delegate to yourself.");

        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;
            require(to != msg.sender, "Found loop in delegation!");
        }

sender is a reference that affects voters[msg.sender].voted. If the delegate has used their ballot, it adds to the total number of received votes. If they haven’t, their weight increases:

Example
sender.voted = true;
        sender.delegate = to;
        Voter storage delegate_ = voters[to];
        if (delegate_.voted) {
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            delegate_.weight += sender.weight;
        }
    }

vote allows giving your vote to a certain proposal, along with any votes you may have been delegated:

Example
function vote(uint proposal) public {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Cannot vote");
        require(!sender.voted, "Has voted.");
        sender.voted = true;
        sender.vote = proposal;
        proposals[proposal].voteCount += sender.weight;
    }

Note: if the proposal you name is not accessible by the array, the function will revert all changes.

Finally, by calling winningProposal() we count the votes received and choose the winner of our blockchain election. To get their index and return a name, we use winnerName():

Example
function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    function winnerName() public view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

Smart Contracts in Auctions

In this section, we will present two Solidity examples of using a smart contract for an open auction. Time for bidding is limited, but everyone can bid until its end. Every bid has to contain Ether to bind them to the bidders. Those who do not win the auction get refunded afterwards.

Simple Auction

We start by including the version pragma and the auction parameters:

Example
pragma solidity >=0.4.22 <0.7.0;

contract SimpleAuction {
    address payable public beneficiary;
    uint public auctionEndTime;

Note: to define time, use either periods in seconds, or a UNIX timestamp.

Then, we define the auction’s current state and allow refunds for outbidded participants. The boolean value shows if the auction has ended. It is false by default, and has to be set to true at the end to put a stop to any possible changes:

Example
address public highestBidder;
    uint public highestBid;

    mapping(address => uint) pendingReturns;

    boolean ended;

Next, we include the constructor with the auction details, and create the events that will trigger in case of a change:

Example
event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    constructor(
        uint _biddingTime,
        address payable _beneficiary
    ) public {
        beneficiary = _beneficiary;
        auctionEndTime = now + _biddingTime;
    }

When calling bid() function, you don’t need to include any arguments: the transaction already contains the data it needs. As you send it, the value you sent along becomes your bid. All bidders who don’t win the auction receive refunds afterwards.

Including require() lets you revert the bidding if the time for it has run out or if there already is a higher bid received:

Example
function bid() public payable {
    
        require(
            now <= auctionEndTime,
            "The auction has ended!"
        );

        require(
            msg.value > highestBid,
            "There is a higher bid!"
        );

Warning: make sure you include the payable keyword: without it, the function won’t be able to receive any Ether!

The safest option for the refunds is allowing the bidders to withdraw the money. You may use highestBidder.send(highestBid) as well, but it is not recommended due to a risk of executing a contract that you do not trust.

Example
if (highestBid != 0) {

            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

For withdrawing a bid that was topped by a higher bidder, you need to include the withdraw() function:

Example
function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {

            pendingReturns[msg.sender] = 0;

Note: defining amount > 0 is crucial to prevent the recipient from re-calling the function before send returns a value.

Now we just need to set the owed amount:

Example
if (!msg.sender.send(amount)) {

                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

When you have functions that have interactions with other contracts, it is generally advised to write them in three steps:

  1. Check the necessary conditions
  2. Perform required actions
  3. Interact with the outside contracts

See how you should follow these steps when ending the auction and sending the highest bid to its beneficiary:

Example
function auctionEnd() public {

        require(now >= auctionEndTime, "Auction hasn’t ended yet.");
        require(!ended, "auctionEnd has already been called.");


        ended = true;
        emit AuctionEnded(highestBidder, highestBid);


        beneficiary.transfer(highestBid);
    }
}

Blind Auction

A blind auction has a few unique features. Instead of an actual bid, a bidder sends its hashed version. There is also no time pressure as the end time of the auction approaches.

The beginning is very similar to that of a simple auction:

Example
pragma solidity >0.4.23 <0.7.0;

contract BlindAuction {
    struct Bid {
        bytes32 blindedBid;
        uint deposit;
    }

    address payable public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

Again, we have to allow the participants to withdraw the bids that didn’t win:

Example
mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

It is recommended to validate function inputs. You can easily do that by using function modifiers. We apply onlyBefore() to the bid() below. The old function body replaces the underscore in the modifier’s body, turning it into a new function body:

Example
modifier onlyBefore(uint _time) { require(now < _time); _; }
    modifier onlyAfter(uint _time) { require(now > _time); _; }

Just like for the simple auction, we have to include a constructor to define auction details:

Example
constructor(
        uint _biddingTime,
        uint _revealTime,
        address payable _beneficiary
    ) public {
        beneficiary = _beneficiary;
        biddingEnd = now + _biddingTime;
        revealEnd = biddingEnd + _revealTime;
    }

To place a blinded bid, you need `_blindedBid` = keccak256(abi.encodePacked(value, fake, secret)). It’s possible to place multiple bids from a single address.

If Ether that you send along with the bid is value and fake is set to false, the bid is considered valid. To hide your real bid but make a deposit, you can either set fake to true, or send a non-exact amount.

Example
function bid(bytes32 _blindedBid)
        public
        payable
        onlyBefore(biddingEnd)
    {
        bids[msg.sender].push(Bid({
            blindedBid: _blindedBid,
            deposit: msg.value
        }));
    }

Warning: you will only get a refund if your bid can be revealed correctly after the auction.

Now we will use reveal() to see the blinded bids. Refunds will be available for all topped bids, as well as invalid bids that were blinded properly:

Example
function reveal(
        uint[] memory _values,
        bool[] memory _fake,
        bytes32[] memory _secret
    )
        public
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(_values.length == length);
        require(_fake.length == length);
        require(_secret.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) {
            Bid storage bidToCheck = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (_values[i], _fake[i], _secret[i]);

If the bid cannot be revealed, there will be no refund as well. We also make sure it’s impossible to claim the same refund more than once:

Example
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
                continue;
            }
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }
            bidToCheck.blindedBid = bytes32(0);
        }
        msg.sender.transfer(refund);
    }

The placeBid() function is internal: that means you can only call it from the contract or others derived from it. See how we make sure to refund the outbid offer:

Example
function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }

Just like with the simple auction, you need to include withdraw() for withdrawing a topped bid and set amount > 0 :

Example
function withdraw() public {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {

            pendingReturns[msg.sender] = 0;

            msg.sender.transfer(amount);
        }
    }

Finally, we finish our auction and send the highest bid to the beneficiary:

Example
function auctionEnd()
        public
        onlyAfter(revealEnd)
    {
        require(!ended);
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }
}

Making a Remote Purchase Safely

You can use a smart contract as an agreement between a seller and a buyer when planning a remote purchase. In this section, we will show you how to do that using an Ethereum contract example.

The main idea is that both seller and buyer send double the value of the item in Ether. When the buyer receives it, they get half of their Ether back. The other half is sent to the seller as payment. Therefore, the seller receives triple the value of their sale, as they also get their Ether refunded.

Solidity Examples

This smart contract also lets both sides block the refund. The pattern you should use for that is the same as withdrawing.

As usual, we start with stating the version pragma and listing the details of our contract:

Example
pragma solidity >=0.4.22 <0.7.0;

contract Purchase {
    uint public value;
    address payable public seller;
    address payable public buyer;
    enum State { Created, Locked, Inactive }
    State public state;

Note: in the second to last line, the default value of the first member also becomes the value of the state variable (State.created).

Next, we will use function modifiers to validate input. We also create three possible events:

  • Aborted(); will trigger if any party cancels the purchase.
  • PurchaseConfirmed(); will trigger when the buyer confirms the purchase.
  • ItemReceived(); will trigger when the buyer receives the item they bought.
Example
constructor() public payable {
        seller = msg.sender;
        value = msg.value / 2;
        require((2 * value) == msg.value, "Value has to be even.");
    }

    modifier condition(bool _condition) {
        require(_condition);
        _;
    }

    modifier onlyBuyer() {
        require(
            msg.sender == buyer,
            "Only buyer can call this."
        );
        _;
    }

    modifier onlySeller() {
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    modifier inState(State _state) {
        require(
            state == _state,
            "Invalid state."
        );
        _;
    }

    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();

Note: make sure to define msg.value in an even number to avoid division truncation.

Before the contract locks, the seller can still abort the sale and reclaim their Ether:

Example
function abort()
        public
        onlySeller
        inState(State.Created)
    {
        emit Aborted();
        state = State.Inactive;
        seller.transfer(address(this).balance);
    }

To confirm the sale, the buyer calls confirmPurchase(). Their transaction has to include double the value of the item in Ether (2 * value):

Example
function confirmPurchase()
        public
        inState(State.Created)
        condition(msg.value == (2 * value))
        payable
    {
        emit PurchaseConfirmed();
        buyer = msg.sender;
        state = State.Locked;
    }

The contract will keep the Ether locked until the buyer calls confirmReceived(). By doing that and confirming the purchased item reached them, the buyer releases the Ether:

Example
function confirmReceived()
        public
        onlyBuyer
        inState(State.Locked)
    {
        emit ItemReceived();
        state = State.Inactive;
        buyer.transfer(value);
        seller.transfer(address(this).balance);
    }
}

Warning: change the state first to prevent contracts from re-calling.

Solidity Examples: Summary

  • While only the Solidity subcurrency creator can issue new tokens, anyone who possesses them can make transactions.
  • During a blockchain election, each ballot can be used by their owner or delegated to a trustee.
  • Smart contracts can be used in creating auctions. You can make a simple open auction, or choose a blind one where the participants send hashed versions of their bid.
  • When you need to sell or purchase something remotely, you can use a Solidity contract as an agreement between both sides.