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.
Contents
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:
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):
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.
event Sent(address from, address to, uint amount);
The constructor
will execute once when you create a contract:
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:
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:
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:
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:
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:
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:
mapping(address => Voter) public voters;
Next, we include a proposal structs array. It will be dynamically-sized:
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:
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:
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:
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:
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:
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()
:
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:
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:
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:
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:
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.
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:
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:
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:
- Check the necessary conditions
- Perform required actions
- Interact with the outside contracts
See how you should follow these steps when ending the auction and sending the highest bid to its beneficiary:
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:
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:
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:
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:
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.
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:
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:
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:
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 :
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:
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.
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:
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.
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:
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
):
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:
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.