Chapter 5. Programming smart contracts in Solidity

This chapter covers

  • Understanding EVM languages
  • Understanding the structure of a contract
  • Learning core Solidity syntax

Thanks to SimpleCoin, the basic cryptocurrency you’ve been building, you’ve learned the basics of Solidity through example. By now, you know Solidity is a high-level EVM language that allows you to write contracts. You also know a smart contract (or, simply, contract) is equivalent to a class in other languages and contains state variables, a constructor, functions, and events. In this chapter, you’ll learn Solidity’s main language constructs in a more structured way and develop a progressively deeper understanding of the language.

This chapter lays the foundation for the next chapter, where you’ll learn how to implement complex contracts and multicontract Dapps in Solidity. By the end of this chapter, you’ll be able to improve and extend SimpleCoin’s functionality with the knowledge you’ve acquired.

5.1. EVM contract languages

Before diving into Solidity, let’s take a little step back and explore briefly some alternative EVM languages. Solidity isn’t the only EVM high-level language for writing contracts. Although it’s the most popular option among Ethereum developers, mainly because it’s frequently upgraded, well maintained, and recommended in the official Ethereum documentation, other alternatives exist, namely LLL, Serpent, and Viper. Let’s see what these languages look like and when it would make sense to use them instead of Solidity.

5.1.1. LLL

LLL, whose acronym stands for lovely little language, is a Lisp-like language (which LLL also stands for) that provides low-level functions close to the EVM opcodes and simple control structures (such as for, if, and so on). These functions allow you to write low-level contract code without having to resort to handwriting EVM assembly. If you’ve ever seen Lisp code or you’re familiar with Clojure, you’ll recognize the distinctive prefix notation and heavy parentheses used in the following LLL listing:

(seq                                      1
  (def 'value 0x00)                       2
  (def 'dummy 0xbc23ecab)                 3
  (returnlll                              4
    (function dummy                       5
      (seq
        (mstore value(calldataload 0))    6
        (return value 32)))))             7

  • 1 Instructs the LLL compiler to evaluate expressions below this line in order
  • 2 Declares a variable named value at memory location 0x00
  • 3 Declares a function called dummy at memory location 0xbc23ecab
  • 4 Defines a macro in the LLL compiler to return the code below it
  • 5 Starts defining the function ‘dummy’
  • 6 Reads the location 0 of the data passed in when calling the function (call data) and stores it in a variable named value
  • 7 Returns 32 bytes from the variable value

This is roughly equivalent to the following Solidity code:

contract Dummy {
    function dummy(bytes32 _value) returns (bytes32) {
        return _value;
    }
}
Warning

Strictly speaking, these two pieces of code aren’t entirely equivalent because the LLL code doesn’t check the function signature or prevent Ether transfer, among other things.

LLL was the first EVM language that the Ethereum core team provided because, given the similarity between how the stack-based Lisp language and the EVM work, it allowed them to deliver it more quickly than any other language. Currently, the main benefit of using LLL would be to get a more compact bytecode, which might be cheaper to run.

After the first public release of the platform, the focus shifted to higher-level languages that would provide a simpler syntax to contract developers. Serpent was the first to be developed.

5.1.2. Serpent and Viper

Serpent is a Python-like language that was popular for a few months after its release. It was praised for its minimalistic philosophy and for offering the efficiency of a low-level language through simple syntax.

If you’re familiar with Python, these are the main limitations you’ll find in Serpent:

  • It doesn’t provide list comprehensions (elegant syntax to create lists from existing sequences and lists) and complex data structures.
  • It doesn’t support first-class functions, therefore limiting your ability to use a functional programming style.

The following listing shows how SimpleCoin would look in Serpent:

def init():
    self.storage[msg.sender] = 10000

def balance_query(address):
    return(self.storage[address])

def transfer(to, amount):
    if self.storage[msg.sender] >= amount:
        self.storage[msg.sender] -= amount
        self.storage[to] += amount

Even if you don’t know Python, you should be able to understand this code. The only variable you might be confused about is self.storage. This is a dictionary containing the contract state. It holds the equivalent of all the state variables in Solidity.

Note

A dict (or dictionary) is the Python implementation of a hash map (or hash table).

Serpent’s popularity began to fade when the focus shifted to Solidity, which programmers started to maintain more regularly. A new experimental Python-like language, called Viper, is currently being researched and is publicly available on GitHub. Its aim is to provide a more extended type set than that offered by Serpent and easier bound and overflow checking on arithmetic operations and arrays. It will also allow you to write first-class functions, although with some limitations, and therefore write more functional code. The main benefit of using Viper is to get more compact and safer bytecode than you’d get from compiling Solidity.

Now that you have a better understanding of EVM languages, let’s move back to Solidity. Open up Remix and enjoy the tour.

5.2. High-level contract structure

Before diving into the various aspects of Solidity, I’ll present the high-level structure of a contract so you can appreciate the purpose of each language feature. This will also give you some context you can refer back to.

5.2.1. Contract declarations

The contract definition for AuthorizedToken, an example token similar to SimpleCoin, shown in the next listing, summarizes all the possible declarations that can appear on a contract. Don’t worry if you don’t fully understand this code: the point of this listing is to give you an idea of what all contract constructs look like.

Listing 5.1. High-level contract structure
pragma solidity ^0.4.24;
contract AuthorizedToken {
    
  enum UserType {TokenHolder, Admin, Owner}        1

  struct AccountInfo {                             2
    address account;
    string firstName;
    string lastName;
    UserType userType;
  }
    
  mapping (address => uint256) 
     public tokenBalance;                          3
  mapping (address => AccountInfo) 
     public registeredAccount;                     3
  mapping (address => bool) 
     public frozenAccount;                         3
    
  address public owner;                            3
    
  uint256 public constant maxTranferLimit = 15000;
    
  event Transfer(address indexed from, 
     address indexed to, uint256 value);           4
  event FrozenAccount(address target, 
     bool frozen);                                 4
    
  modifier onlyOwner {                             5
    require(msg.sender == owner);
    _;
  }
    
  constructor(uint256 _initialSupply) public {     6
    owner = msg.sender;

    mintToken(owner, _initialSupply);   
  }

  function transfer(address _to, uint256 _amount) 
    public {                                            7
    require(checkLimit(_amount));
    //... 
    emit Transfer(msg.sender, _to, _amount);
  }  
  function registerAccount(address account, 
    string firstName,                                   7
    string lastName, bool isAdmin) public onlyOwner {
    //...
  }
    
  function checkLimit(uint256 _amount) private          7
    returns (bool) { 
    if (_amount < maxTranferLimit) 
        return true;
    return false;
  }

  function validateAccount(address _account) 
    internal                                            7
    returns (bool) { 
    if (frozenAccount[_account] && tokenBalance[_account] > 0) 
        return true;
    return false;
  }

  function mintToken(address _recipient, 
    uint256  _mintedAmount)                             8
    onlyOwner public  { 
    tokenBalance[_recipient] += _mintedAmount;
    emit Transfer(owner, _recipient, _mintedAmount);
    }

  function freezeAccount(address target, 
    bool freeze)                                
    onlyOwner public  { 
    frozenAccount[target] = freeze; 
    emit FrozenAccount(target, freeze);
  }
}

  • 1 Enum definition
  • 2 Struct definition
  • 3 State variable definitions
  • 4 Event definitions
  • 5 Function modifier definition
  • 6 Constructor definition
  • 7 Function definitions
  • 8 Functions defined with modifiers

In summary, these are the possible items you can declare:

  • State variables
  • Events
  • Enums
  • Struct types
  • Functions
  • Function modifiers

Let’s go quickly through each of them. After we complete a high-level contract overview, we’ll delve into each language feature.

State variables

State variables hold the contract state. You can declare them with any of the types that the language supports. Some types, such as mapping, are only allowed for state variables. The declaration of a state variable also includes, explicitly or implicitly, its access level.

Events

An event is a contract member that interacts with the EVM transaction log and whose invocation is then propagated to clients subscribed to it, often triggering related callbacks. An event declaration looks more similar to a declaration of Java or C# events than a declaration of JavaScript events.

Enums

An enum defines a custom type with a specified set of allowed values. An enum declaration is similar to that of Java and C# enums.

Struct types

A struct defines a custom type that includes a set of variables, each in general of a different type. A struct declaration is similar to that of a C struct.

Functions

Functions encapsulate the logic of a contract, are altered by modifiers, have access to state variables, and can raise the events declared on the contract.

Function modifiers

A function modifier allows you to modify the behavior of a function, typically to restrict its applicability to only certain input, in a declarative way. A contract might declare many modifiers that you might use on several functions.

5.3. Solidity language essentials

During this initial tour of Solidity, you’ll get a firm foundation in the language by learning about most of its syntax and constructs:

  • Data types
  • The global namespace
  • State variables
  • Functions
  • Function modifiers
  • Variable declaration, initialization, and assignment
  • Events
  • Conditional statements

You’ll explore more advanced object-oriented features and concepts in the next chapter.

Like most statically typed languages, Solidity requires you to explicitly declare the type of each variable, or at least needs the type to be inferred unequivocally by the compiler. Its data type system includes both value types and reference types, which I’ll present in the next few sections.

5.3.1. Value types

A value type variable is stored on the EVM stack, which allocates a single memory space to hold its value. When a value type variable is assigned to another variable or passed to a function as a parameter, its value is copied into a new and separate instance of the variable. Consequently, any change in the value of the assigned variable doesn’t affect the value of the original variable. Value types include most native types, enums, and functions.

bool

Variables declared as bool can have either a true or false value; for example

bool isComplete = false;
Note

Logical expressions must resolve to true or false, so trying to use integer values of 0 and 1 for false and true, as in JavaScript, C, or C++, isn’t allowed in Solidity.

Integer types

You can declare integer variables either as int (signed) or uint (unsigned). You also can specify an exact size, ranging from 8 to 256 bits, in multiples of 8. For example, int32 means signed 32-bit integer, and uint128 means unsigned 128-bit integer. If you don’t specify a size, it’s set to 256 bits. The sidebar explains how the assignment between variables of different integer types works.

Implicit and explicit integer conversions

Assignments between variables of different integer types is possible only if it’s meaningful, which generally means the type of the receiving variable is less restrictive or is larger. If that’s the case, an implicit conversion happens. The contract shown here, which you can enter into the Remix editor, shows some examples of valid and invalid assignments leading to implicit conversions when successful:

contract IntConversions {
    int256 bigNumber = 150000000000;

    int32 mediumNegativeNumber = -450000;
    uint16 smallPositiveNumber = 15678;
    int16 newSmallNumber = bigNumber;           1
    uint64 newMediumPositiveNumber = 
      mediumNegativeNumber;                     2
    uint32 public newMediumNumber = 
      smallPositiveNumber;                      3
    int256 public newBigNumber = 
      mediumNegativeNumber;                     4
}

  • 1 Compile error because newSmallNumber is too small to contain bigNumber
  • 2 Compiler error because uint64 can only hold positive numbers
  • 3 smallPositiveNumber is implicitly converted from uint16 to uint32; newMediumNumber =15,678
  • 4 mediumNegativeNumber is implicitly converted from int32 to int256; newBigNumber =-450,000

To compile this code, remove the two lines that are causing errors, such as

TypeError: type int256 isn’t implicitly convertible to expected type int16

Then instantiate the contract (click Deploy). Finally, get the values of newMedium-Number and newBigNumber by clicking the corresponding buttons.

When an implicit conversion isn’t allowed, it’s still possible to perform explicit conversions. In such cases, it’s your responsibility to make sure the conversion is meaningful to your logic. To see an example of explicit conversions in action, add the following two lines to the IntConversions contract:

    int16 public newSmallNumber = 
       int16(bigNumber);                       1
    uint64 public newMediumPositiveNumber =
       uint64(mediumNegativeNumber);           2

  • 1 The explicit conversion and assignment are successful, but newSmallNumber = 23,552.
  • 2 The explicit conversion and assignment are successful, but newMediumPositiveNumber = 18,446,744,073,709,101,616.

Reinstantiate the contract (by clicking Create again), then get the value of newSmallNumber and newMediumPositiveNumber by clicking the corresponding buttons. The results of the explicit integer conversions aren’t intuitive: they wrap the original value around the size of the target integer type (if its size is smaller than that of the source integer type) rather than overflowing.

The principles behind implicit and explicit conversions between integer types apply also to other noninteger types.

Static byte arrays

You can declare byte arrays of a fixed size with a size ranging from 1 to 32—for example, bytes8 or bytes12. By itself, byte is an array of a single byte and is equivalent to bytes1.

Warning

If no size is specified, bytes declares a dynamic size byte array, which is a reference type.

Address

Address objects, which you generally declare using a literal containing up to 40 hexadecimal digits prefixed by 0x, hold 20 bytes; for example:

address ownerAddress = 0x10abb5EfEcdC09581f8b7cb95791FE2936790b4E;
Warning

A hexadecimal literal is recognized as an address only if it has a valid checksum. This is determined by hashing the hexadecimal literal with the sha3 function (provided by the Web3 library) and then verifying that the alphabetic characters in the literal are uppercase or lowercase, depending on the value of the bits in the hash at the same index position. This means an address is case-sensitive and you can’t validate it visually. Some tools, such as Remix, will warn you if an address isn’t valid but will still process an invalid address.

It’s possible to get the Ether balance in Wei (the smallest Ether denomination) associated with an address by querying the balance property. You can try it by putting the following sample contract in Remix and executing the getOwnerBalance() function:

contract AddressExamples {
    address ownerAddress = 0x10abb5EfEcdC09581f8b7cb95791FE2936790b4E;
    
    function getOwnerBalance() public returns (uint)  {
        uint balance = ownerAddress.balance;
        return balance;
    }
}

You’ll see the return value in the output panel at the bottom left of the Remix screen, after you click the Details button next to the output line corresponding to the function call. The address type exposes various functions for transferring Ether. Table 5.1 explains their purposes.

Table 5.1. Functions provided by the address type

Function

Purpose

transfer() To transfer Ether in Wei—If the transaction fails on the receiving side, an exception is thrown to the sender and the payment is automatically reverted (although any spent gas isn’t refunded), so no error handling code is necessary; transfer() can spend only up to a maximum of 2300 gas.
send() To send Ether in Wei—If the transaction fails on the receiving side, a value of false is returned to the sender but the payment isn’t reverted, so it must be handled correctly and reverted with further instructions; send() can spend only up to a maximum of 2,300 gas.
call() To invoke a function on the target contract associated with the address (the target account is assumed to be a contract)—An amount of Ether can be sent together with the function call by specifying it in this way: call.value(10)("contractName", "functionName"); call() transfers the entire gas budget from the sender to the called function. If call() fails, it returns false, so failure must be handled in a similar way to send().
Warning

Because of security concerns, send() and call() are being deprecated, and it won’t be possible to use them in future versions of Solidity.

This is an example of an Ether transfer using transfer():

destinationAddress.transfer(10);        1

  • 1 Sends 10 Wei to destinationAddress

If sending Ether with send(), you must have error handling to avoid losing Ether:

if (!destinationAddress.send(10))
    revert();                       1

  • 1 If the send() operation fails, it returns false and must be handled, in this case by reverting the state and consequently the payment.

Another way of ensuring a transfer failure reverts the payment is by using the global require() function, which reverts the state if the input condition is false:

require(destinationAddress.send(10));       1

  • 1 Reverts the payment if send fails and returns false

You can invoke a function on an external contract with call() as follows:

destinationContractAddress.call("contractName", 
   "functionName");                             1

  • 1 Invokes an external contract function

You can send Ether during the external call as follows:

destinationContractAddress.call.value(10)(
   "contractName", "functionName");              1

  • 1 Sends an amount of Ether expressed in Wei with the external call() by specifying it with value()

You must have handling for a failure of the external function call, as for send(), to ensure the state (including Ether payment) is reverted:

if (!destinationContractAddress.call.value(10)("contractName"
 , "functionName"))
    revert();                 1

  • 1 Reverts the state and consequently the payment

Chapter 15 on security will cover in detail how to invoke transfer(), send(), and call() correctly and how to handle errors safely.

Enums

An enum is a custom data type including a set of named values; for example:

enum  InvestmentLevel {High, Medium, Low}

You can then define an enum-based variable as follows:

InvestmentLevel level = InvestmentLevel.Medium;

The integer value of each enum item is implicitly determined by its position in the enum definition. In the previous example, the value of High is 0 and the value of Low is 2. You can retrieve the integer value of an enum type variable by explicitly converting the enum variable to an int variable as shown here:

InvestmentLevel level = InvestmentLevel.Medium;
...
int16 levelValue = int16(level);

Implicit conversions aren’t allowed:

int16 levelValue = level;         1

  • 1 Doesn’t compile

5.3.2. Reference types

Reference type variables are accessed through their reference (the location of their first item). You can store them in either of the following two data locations, which you can, in some cases, explicitly specify in their declaration:

  • Memory—Values aren’t persisted permanently and only live in memory.
  • Storage—Values are persisted permanently on the blockchain, like state variables.

A third type of data location is available that you can’t explicitly specify:

  • Calldata—This is an area dedicated to holding the values of function parameters of external functions. Objects held in this area behave like objects stored in memory.

The following listing shows various reference type variables declared in different data locations.

Listing 5.2. Reference types with location declared implicitly or explicitly
pragma solidity ^0.4.0;
contract ReferenceTypesSample {
    uint[] storageArray;                              1
    
    function f(uint[] fArray) {}                      2
    function g(uint[] storage gArray) internal {}     3
    function h(uint[] memory hArray) internal  {}     4
}

  • 1 The data location of storageArray implicitly defined as storage
  • 2 The data location of fArray implicitly defined as memory
  • 3 The data location of gArray explicitly defined as storage
  • 3 The data location of hArray explicitly defined as memory

Before looking at code snippets focused on data locations, have a look at table 5.2, which summarizes the default data location of variables, depending on whether they’re local or state variables, and of function parameters, depending on whether the function has been declared internal or external.

Table 5.2. Default data location of variables and function parameters

Case

Data location

Default

Local variable Memory or storage Storage
State variable Only storage Not applicable
Parameter of internal function Memory or storage Memory
Parameter of external function Calldata Not applicable

The behavior of reference type variables, specifically whether they get cloned or referenced directly when assigned to other variables or passed to function parameters, depends on the source and target data location. The best way to understand what happens in the various cases is to look at some code. The following code snippets all assume the ReferenceTypesSample contract definition given in listing 5.2.

The first case is the assignment of a state variable (whose data location is, as you know, the storage) to a local variable:

function f(uint[] fArray)  {
     uint[] localArray = storageArray;       1
}

  • 1 localArray is defined implicitly in the storage and points directly to storageArray.

If localArray is modified, storageArray is consequently modified.

The next example is the assignment of a function parameter defined in memory to a local variable:

function f(uint[] fArray)  {           1
     uint[] localArray = fArray;       2
}

  • 1 fArray is implicitly defined in memory.
  • 2 localArray is defined in memory and points directly to fArray.

If localArray is modified, fArray is consequently modified.

The following example shows what happens if you assign a function parameter defined in memory to a storage variable:

function f(uint[] fArray)  {     1
    storageArray= fArray;        2
}

  • 1 fArray is implicitly defined in memory.
  • 2 storageArray stores a full copy of fArray.

If you pass a state variable to a function parameter defined in storage, the function parameter directly references the state variable:

function f(uint[] fArray)  {                    1
     g(storageArray);                           2
}
function g(uint[] storage gArray) internal {}   3

  • 1 fArray is implicitly defined in memory.
  • 2 During the call, gArray points directly to storageArray.
  • 3 gArray is defined explicitly in storage.

If gArray is modified, storageArray is consequently modified.

If you pass a state variable to a function parameter defined in memory, the function parameter creates a local clone of the state variable:

function f(uint[] fArray)  {                    1
    h(storageArray);                            2
}
function h(uint[] memory hArray) internal {}    3

  • 1 fArray is implicitly defined in memory.
  • 2 During the call, hArray is assigned to a clone of storageArray.
  • 3 hArray is defined explicitly in memory.

There are four classes of reference types:

  • Arrays
  • Strings
  • Structs
  • Mappings

Arrays can be static (of fixed size) or dynamic and are declared and initialized in slightly different ways.

Static arrays

You must specify the size of a static array in its declaration. The following code declares and allocates a static array of five elements of type int32:

function f(){
   int32[5] memory fixedSlots;
   fixedSlots[0] = 5;
   //...        
}

You can also allocate a static array and set it inline, as follows:

function f(){
   int32[5] memory fixedSlots = [int32(5), 9, 1, 3, 4];
}
Note

Inline arrays are automatically defined as to memory data location and are sized with the smallest possible type of their biggest item. In the inline static array example, imagine you hadn’t enforced the item in the first cell as int32: int32[5] memory fixedSlots = [5, 9, 1, 3, 4]. In this case, the inline array would have been implicitly declared as int4[] memory, and it would have failed the assignment to the fixedSlots variable. Therefore, it would have produced a compilation error.

Dynamic arrays

You don’t need to specify the size in the declaration of dynamic arrays, as shown in the following snippet:

function f(){
   int32[] unlimitedSlots;
}

You can then append items to a dynamic array by calling the push member function:

unlimitedSlots.push(6);
unlimitedSlots.push(4);

If you need to resize a dynamic array, you must do so in different ways depending on whether its data location is memory or storage. If the data location is storage, you can reset its length, as shown in the following snippet:

function f(){
   int32[] unlimitedSlots;       1
   //...

   unlimitedSlots.length = 5;    2
}

  • 1 Implicitly declares it with storage data location
  • 2 Resizes it by resetting its length

If the data location of a dynamic array is memory, you have to resize it with new, as shown in the following snippet:

function f(){
  int32[] memory unlimitedSlots;       1
  //...
  unlimitedSlots = new int32[](5);     2
}

  • 1 Explicitly declares it with memory data location
  • 2 Resizes it with new
Note

As you saw earlier, bytes is an unlimited byte array and is a reference type. This is equivalent to byte[], but it’s optimized for space, and its use is recommended. It also supports length and push().

If an array is exposed as a public state variable, its getter accepts the array positional index as an input.

String

string is in fact equivalent to bytes but with no length and push() members. You can initialize it with a string literal:

string name = "Roberto";
Struct

A struct is a user-defined type that contains a set of elements that in general are each of a different type. The following listing shows a contract declaring various structs that get referenced in its state variables. This example also shows how you can use enums in a struct.

Listing 5.3. Contract containing various struct definitions
contract Voting {
 
    enum UserType {Voter, Admin, Owner}

    enum MajorityType {SimpleMajority, AbsoluteMajority, SuperMajority
 , unanimity}

    struct UserInfo {
        address account;
        string name;
        string surname;

        UserType uType;
    }

    struct Candidate {
        address account;
        string description;
    }

    struct VotingSession {
        uint sessionId;
        string description;
        MajorityType majorityType;
        uint8 majorityPercent;
        Candidate[] candidates;
        mapping (address => uint) votes;
    }

    uint numVotingSessions;
    mapping (uint => VotingSession) votingSessions;

    //...
}

You can initialize a struct object as follows:

    function addCandidate(uint votingSessionId, 
    address candidateAddress, 
    string candidateDescription) {
        Candidate memory candidate =
        Candidate({account:candidateAddress,
     description:candidateDescription});

        votingSessions[votingSessionId].candidates.push(candidate);
    }
Mapping

mapping is a special reference type that you can only use in the storage data location, which means you can declare it only as a state variable or a storage reference type. You might remember mapping is the Solidity implementation of a hash table, which stores values against keys. The hash table is strongly typed, which means you must declare the type of the key and the type of the value at its declaration:

mapping(address => int) public coinBalance;

In general, you can declare the value of any type, including primitive types, arrays, structs, or mappings themselves.

Contrary to hash table implementations of other languages, mapping has no contains-Key() function. If you try to get the value associated with a missing key, it will return the default value. For example, your coinBalance mapping will return 0 when trying to get the balance of an address missing from the mapping:

int missingAddressBalance = 
coinBalance[0x6C15291028D082...];     1

  • 1 missingAddressBalance == 0;

This completes your tour of data types. You’ve seen how to declare and instantiate value type and reference type variables. A certain set of variables are declared implicitly, and you can always access them from your contract. They’re part of the so-called global namespace that we’re going to explore next.

5.3.3. Global namespace

The global namespace is a set of implicitly declared variables and functions that you can reference and use in your contract code directly.

Implicitly declared variables

The global namespace provides the following five variables:

  • block holds information about the latest blockchain block.
  • msg provides data about the incoming message.
  • tx provides transaction data.
  • this is a reference to the current contract. You can use it to call internal functions as if they were defined external and therefore store the message call on the blockchain. (internal and external are function accessibility levels that I’ll explain later, in the functions section.) If you use it by itself, it’s implicitly converted to the address of the current contract.
  • now is the time associated with the creation of the latest block, expressed as a Unix epoch.

Table 5.3 summarizes the functions and properties that global variables expose.

Table 5.3. Members of the main global variables

Global variable

Type

Member

Return type

Description

block function blockhash(uint blocknumber) bytes32 Hash of given block (only available for last 256 blocks, as specified in the Yellow Paper, for simplicity of design and performance reasons)
property coinbase address Block’s miner’s address
property gaslimit uint Block’s gas limit
property number uint Block’s number
property timestamp uint Block’s timestamp as UNIX epoch
msg property data bytes Full calldata body
property sender address Message sender (who is performing the current call)
property gas uint Remaining gas
property value uint Amount of Ether sent with the message, in Wei
tx property gasprice uint Transaction gas price
property origin address Transaction sender (who originated the full call chain)
now property N/A uint Although the name of this variable might lead you to believe that this might return the current time (perhaps as a UNIX epoch), now is in fact an alias for block.timestamp, which is the time at which the current latest block was created.
Implicitly declared functions

The following two functions, available from the global namespace, throw an exception and revert the contract state if the associated condition isn’t met. Although they work exactly the same way, their intention is slightly different:

  • require(bool condition)—This is used to validate function input. You’ve already seen it when validating the input of SimpleCoin.transfer().
  • assert(bool condition)—This is used to validate contract state or function state.

You can also terminate the execution and revert the contract state explicitly by calling revert().

If you want to remove the current contract instance from the blockchain, for example because you’ve realized your contract has a security flaw that’s being actively exploited by hackers, you can call selfdestruct(Ether recipient address). This will uninstall the current instance from the blockchain and move the Ether present at the associated account to the specified recipient address.

What does uninstalling a contract mean in the context of a blockchain? It means that the contract will be removed from the current state of the blockchain and will become unreachable. But its trace will remain in the blockchain history. The contract is considered fully removed only after the selfdestruct(recipient address) transaction has been mined and the related block has been propagated throughout the network.

Warning

You should be aware that the recipient address has no way of rejecting Ether coming from a selfdestruct() call; the destruction of the contract and the crediting of the recipient account are a single atomic operation. As you’ll see in a later chapter dedicated to security, this can be maliciously exploited to perform sophisticated attacks.

The global namespace also provides various cryptographic hash functions, such as sha256()(from the SHA-2 family) and keccak256() (from the SHA-3 family). More on those in a later chapter, but for now let’s move on to state variables.

5.3.4. State variables

You already know state variables hold the contract state. What I haven’t covered so far is the access level that you can specify when declaring them. Table 5.4 summarizes the available options.

Table 5.4. Access levels of a state variable

Access level

Description

public The compiler automatically generates a getter function for each public state variable. You can use public state variables directly from within the contract and access them through the related getter function from external contract or client code.
internal The contract and any inherited contract can access Internal state variables. This is the default level for state variables.
private Only members of the same contract—not inherited contracts—can access private state variables.

The StateVariablesAccessibility contract in the following listing shows examples of state variable declarations, including their accessibility level.

Listing 5.4. Examples of state variables declared with various accessibility levels
pragma solidity ^0.4.0;
contract StateVariablesAccessibility {
    mapping (address => bool) 
    private frozenAccounts;                             1
    uint isContractLocked;                              2
    mapping (address => bool) public tokenBalance;      3

    ...
}

  • 1 You can access frozenAccount only from within this contract, not from inherited contracts.
  • 2 isContractLocked is implicitly defined as internal, so it’s accessible from within this contract and inherited contracts.
  • 3 tokenBalance is accessible externally, and the Solidity compiler automatically generates a getter function.
Constant state variables

It’s possible to declare a state variable as constant. In this case, you have to set it to a value that isn’t coming from storage or from the blockchain in general, so values from other state variables or from properties of the block global variable aren’t allowed.

This code shows some examples of constant state variables:

pragma solidity ^0.4.0;
contract ConstantStateVariables {
    uint constant maxTokenSupply = 10000000;           1
    string constant contractVersion ="2.1.5678";       1
    bytes32 constant contractHash = 
        keccak256(contractVersion, maxTokenSupply);    2
    ...
 }

  • 1 You can declare a value type or string state variable as constant.
  • 2 You can assign the result of a stateless built-in mathematical or cryptographic function to a constant state variable.

Although you’ve already come across functions, there are various aspects of functions that I haven’t covered yet. I’ll cover them in the next section.

5.3.5. Functions

You can specify function input and output parameters in various ways. Let’s see how.

Input parameters declaration

You declare input parameters in Solidity, as in other statically typed languages, by providing a list of typed parameter names, as shown in the following example:

    function process1(int _x, int _y, int _z, bool _flag) {
    ...
    }

If you don’t use some of the parameters in the implementation, you can leave them unnamed (or anonymous), like the second and third parameter in the following example:

    function process2(int _x, int, int, bool _flag) {
        if (_flag)
           stateVariable = _x;
    }

You’ll understand better the purpose of anonymous parameters in chapter 6, when you’ll learn about abstract functions of abstract contracts and how to override them in concrete contracts. (Some overridden functions might not need all the parameters specified in the abstract function of the base abstract contract.)

Note

You might find, as I did initially, the naming convention for parameters a bit odd, because in other languages, such as Java or C#, you might have used an underscore prefix to identify member variables. In Solidity, an underscore prefix is used to identify parameters and local variables. But it seems this convention is fading away, and underscore prefixes might disappear altogether from Solidity naming conventions.

Output parameters declaration

In Solidity, a function can in general return multiple output parameters, in a tuple data structure. You specify output parameters after the returns keyword and declare them like input parameters, as shown here:

    function calculate1(int _x, int _y, int _z, bool _flag) 
        returns (int _alpha, int _beta, int _gamma) {          1
        _alpha = _x + _y;                                      2
        _beta = _y + _z;                                       2
        if (_flag)
            _gamma = _alpha / _beta;                           2
        else 
            _gamma = _z;                                       2
    }

  • 1 Here, _alpha, _beta, and _gamma are declared as returned parameters.
  • 2 No return statement is necessary to return the result tuple to the caller.
Note

A tuple is an ordered list of elements, in general each of a different type. This is an example of a tuple: 23, true, "PlanA", 57899, 345

As you can see in the code example, contrary to most languages, no return statement is necessary when you can write the logic in such a way that you’ve set all output parameters correctly before the execution of the function is complete. You can think of the output parameters as local variables initialized to their default value, in this case 0, at the beginning of the function execution. If you prefer, though, and if the logic requires you to do so, you can return output from a function using return, as shown here:

    function calculate2(int _x, int _y, int _z, bool _flag) 
        returns (int, int, int) {                           1
        int _alpha = _x + _y;                               2
        int _beta = _y + _z;                                2
        if (_flag)
             return (_alpha, _beta, _alpha / _beta);        3
     
        return (_alpha, _beta, _z);                         3
    }

  • 1 Only the types of the return tuple are declared.
  • 2 You define and assign _alpha and _beta in the body of the function.
  • 3 The result tuple is returned to the caller explicitly with a return statement.
Function access levels

As for state variables, functions also can be declared with different access levels, as summarized in table 5.5.

Table 5.5. Access levels of a function

Access level

Description

external An external function is exposed in the contract interface, and you can only call it from external contracts or client code but not from within the contract.
public A public function is exposed in the contract interface, and you can call it from within the contract or from external contracts or client code. This is the default accessibility level for functions.
internal An internal function isn’t part of the contract interface, and it’s only visible to contract members and inherited contracts.
private A private function can only be called by members of the contract where it’s been declared, not by inherited contracts.

The following contract code shows some function declarations, including the accessibility level:

contract SimpleCoin {
    function transfer(address _to, uint256 _amount) 
        public {}                                           1
 
    function checkLimit(uint256 _amount) private 
        returns (bool) {}                                   2

    function validateAccount(address avcount) internal 
        returns (bool) {}                                   3
  
    function freezeAccount(address target, bool freeze) 
        external {}                                         4
}

  • 1 A public function, accessible internally and externally
  • 2 A private function, only accessible from within this contract
  • 3 A private function, accessible from this and inherited contracts
  • 3 An external function, only accessible externally
Internal function invocation

Functions can be invoked internally or externally. For example, a function can invoke another function directly within the same contract, as shown here:

contract TaxCalculator {
    function calculateAlpha(int _x, int _y, int _z) 
         public returns (int _alpha)  {
        _alpha = _x + calculateGamma(_y, _z);       1
    }


    function calculateGamma(int _y, int _z) 
         internal returns (int _gamma) {            2
        _gamma  = _y *3 +7*_z;
    }
}

  • 1 This invocation results in a call: _y and _z are accessed directly through memory references.
  • 2 This is an internal function, not accessible from outside the contract.

This way of invoking a function is known as a call. With a call, the body of the function accesses parameters directly through memory references.

External function invocation

A function can call a function of an external contract through the contract reference, as shown here:

contract GammaCalculator {                              1
    function calculateGamma(int _y, int _z) 
        external returns (int _gamma) {
        _gamma  = _y *3 +7*_z;
    }
}

contract TaxCalculator2 {
    
    GammaCalculator gammaCalculator;
    
    function TaxCalculator(address _gammaCalculatorAddress) {
        gammaCalculator = GammaCalculator(_
           gammaCalculatorAddress);                     2
    }
    
    function calculateAlpha(int _x, int _y, int _z) 

        public returns (int _alpha) {
        _alpha = _x + gammaCalculator.calculateGamma(
           _y, _z);                                     3
    }
}

  • 1 GammaCalculator is an external contract with respect to TaxCalculator2.
  • 2 gammaCalculator points to an instance deployed at address _gammaCalculatorAddress.
  • 3 This is an external function invocation, which results in a ‘transaction message’ that gets stored on the blockchain.

In this case, parameters are sent to GammaCalculator through a transaction message that’s then stored on the blockchain, as you can see in the sequence diagram in figure 5.1.

Figure 5.1. Sequence diagram illustrating an external function invocation. The calculateAlpha() function of the TaxCalculator2 contract calls the external calculateGamma() function on the GammaCalculator contract. Because the call is external, the function parameters are sent to the external contract through a transaction that’s stored on the blockchain.

You can force a call to a public function to appear as an external invocation, and therefore execute through a transaction message, if it’s performed through this, the reference to the current contract, as shown here:

contract TaxCalculator3 {
   function calculateAlpha(int _x, int _y, int _z) 
      public returns (int _alpha) {
       _alpha = _x + this.calculateGamma(_y, _z);      1
   }

   function calculateGamma(int _y, int _z) 
      public returns (int _gamma) {                    2
       _gamma  = _y *3 +7*_z;
   }
}

  • 1 The call on calculateGamma through 'this' behaves as a call on an external contract, so a transaction message is generated and this gets stored on the blockchain.
  • 2 To be called through this, you must declare calculateGamma as public.
Changing parameters order at function invocation

When invoking a function, you can pass the parameters in any order if you specify their name, as shown in the following listing:

contract TaxCalculator4 {
   function calculateAlpha(int _x, int _y, int _z)
      public returns (int _alpha) {
       _alpha = _x + this.calculateGamma(
          {_z:_z, _y:_y});                       1
  }

  function calculateGamma(int _y, int _z) 
     public returns (int _gamma) {
       _gamma  = _y *3 +7*_z;
    }
}

  • 1 You can pass the parameters of calculateGamma in an arbitrary order, but in this case, you must also specify the parameter names.
View and pure functions

It’s possible to declare a function as view, with the intent that it doesn’t perform any action that might modify state, as defined in table 5.6. But the compiler doesn’t check whether any state modification takes place, so the view keyword is currently used on functions mainly for documentation purposes.

Table 5.6. Actions that lead to a state modification

State modifying action

Writing to state variables
Raising events
Creating or destroying contracts
Transferring Ether (through send() or transfer())
Calling any function not declared as view or pure
Using low-level calls (for example call()) or certain inline assembly opcodes
Note

In earlier versions of Solidity, the view keyword was named constant. Many developers argued that constant was misleading because it wasn’t clear whether it meant, as in other languages, that the function would return only constant results. So, although you can still use constant instead of view, the latter is recommended.

It’s possible to declare a function as pure, with the intent that it doesn’t perform any action that might modify state (as seen for view functions) or read state, as defined in table 5.7. As with view functions, the compiler doesn’t check that pure functions don’t modify or read state, so for now, the pure keyword has only a documentation purpose.

Table 5.7. Actions that can be interpreted as reading from state

State reading actions

Reading from state variables
Accessing account balance (through this.balance or address.balance)
Accessing the members of block, tx, and most of the members of msg
Calling any function not declared as pure
Using certain inline assembly opcodes

The code in the following listing highlights in bold, functions of listing 5.1 that you can declare as view or pure.

Listing 5.5. Functions you can declare as view or pure
pragma solidity ^0.4.24;
contract AuthorizedToken {
    
    //...
    
    mapping (address => uint256) public tokenBalance; 
    mapping (address => bool) public frozenAccount;
    
    address public owner;                                 1
    uint256 public constant maxTranferLimit = 50000;
    
    //...

    function transfer(address _to, uint256 _amount) public { 
        require(checkLimit(_amount));
        //... 
        tokenBalance[msg.sender] -= _amount;              2
        tokenBalance[_to] += _amount;                     2
        Transfer(msg.sender, _to, _amount);
    }  
    
    //...
    
    function checkLimit(uint256 _amount) private pure
        returns (bool) { 
        if (_amount < maxTranferLimit)                    3
            return true;
        return false;
    }

    function validateAccount(address _account) internal view
        returns (bool) { 
        if (frozenAccount[_account] 
            && tokenBalance[_account] > 0)                1
            return true;
        return false;
    }
   
    //...

    function freezeAccount(address target, bool freeze)
        onlyOwner public  { 
        frozenAccount[target] = freeze;                   4
        FrozenAccount(target, freeze);
    }
}

  • 1 Doesn’t alter state variable, so you can declare this function as view
  • 2 Alters the state variable tokenBalance, so you can’t declare transfer() as view or pure
  • 3 Doesn’t read or alter state, so you can declare this function as pure
  • 3 Alters state variable frozenAccount, so you can’t declare freezeAccount() as view or pure
Payable functions

You declare a function as payable if you want to allow it to receive Ether. The following example shows how to declare a function as payable:

contract StockPriceOracle {
   uint quoteFee = 500;                                1
   mapping (string => uint) private stockPrices;  
   
   //...
   
   function getStockPrice(string _stockTicker) 
        payable returns (uint _stockPrice) {
        if (msg.value == quoteFee)                     2
        {
            //...
            _stockPrice = stockPrices[_stockTicker];
        }
        else 
            revert();                                  3
    }
}

  • 1 Quote price in Wei, which the caller must send when invoking getStockPrice()
  • 2 Checks the Ether amount sent (in Wei) to cover the fee for the service to verify it’s correct
  • 3 If the fee sent is incorrect, reverts the transaction and the sender isn’t charged

The following code shows how to send Ether together with the input when calling the getStockPrice() function:

address stockPriceOracleAddress = 0x10abb5EfEcdC09581f8b7cb95791FE2936790b4E;
uint256 quoteFee = 500;
string memory stockTicker = "MSFT";
 
if (!stockPriceOracleAddress.call.value(quoteFee)
    (bytes4(sha3("getStockPrice()")), 
     _stockTicker))                      1
    revert();                            2

  • 1 Sends Ether while invoking an external function with call()
  • 2 If call() fails, it returns false, and the state is reverted with revert();.
Fallback function

A contract can declare one unnamed (or anonymous) payable function that can’t have any input or output parameters. This becomes a fallback function in case a client call doesn’t match any of the available contract functions, or in case only plain Ether is sent to the contract via send(), transfer(), or call().

The gas budget transferred to the fallback function is minimal if you call the fallback function by send() or transfer(). In this case, its implementation must avoid any costly operations, such as writing to storage, sending Ether, or calling internal or external functions that have complex or lengthy logic. A send() or transfer() call on a nonminimal fallback implementation is likely to run out of gas and fail almost immediately. You must avoid this situation because it puts Ether at risk of getting lost or even stolen, as you’ll see in the chapter dedicated to security.

The following code shows the classic minimal fallback function implementation, which allows incoming send() and transfer() calls to complete an Ether transfer successfully:

contract A1 {
    function () payable {}
}

You also can implement a fallback function so that it prevents the contract from accepting Ether if it isn’t meant to:

contract A2 {
    function () payable  {
        revert();
    }
}
Warning

As you’ll see in the chapter dedicated to security, the fallback function offers malicious participants various ways of attacking a contract, so if you decide to provide a fallback, you must learn how to implement it correctly.

Getter functions

As I mentioned earlier, the compiler automatically generates a getter function for each public state variable declared in the contract. The getter function gets the name of the state variable it exposes. For example, given the usual contract

contract SimpleCoin {
    mapping (address => uint256) public coinBalance;       
    //...
}

you can consult the balance of an account as follows:

uint256 myBalance = simpleCoinInstance.coinBalance(myAccountAddress);

A getter function is implicitly declared as public and view, so it’s possible to invoke it from within the contract through this, as shown in the following code:

contract SimpleCoin {
    mapping (address => uint256) public coinBalance;       
    //...

    function isAccountUsed(address _account) 
        internal view returns (bool) {
        if (this.coinBalance(_account) > 0)       1
            return true;
        return false;
    }
}

  • 1 This check is performed with a getter, but you could have it performed by accessing the mapping directly: this.coinBalance[_account], the only difference being parentheses versus square brackets.

You can alter the behavior of functions with function modifiers. Keep reading to see how.

5.3.6. Function modifiers

A function modifier alters the behavior of a function by performing some pre- and postprocessing around the execution of the function using it. As an example of a preprocessing modifier, the code in listing 5.6 shows onlyOwner, a typical modifier that allows the function to be called only if the caller is the contract owner, which is the account that instantiated the contract. isActive is a parameterized modifier that checks if the input user account isn’t frozen.

Listing 5.6. Example of a contract with function modifiers
contract FunctionModifiers {
    address owner;
    address[] users;
    mapping (address => bool) frozenUser;

    function FunctionModifiers () {
        owner = msg.sender;                    1
    }

    modifier onlyOwner {                       2
        require(msg.sender == owner);
        _;
    }

    modifier isActive(address _account) {
        require(!frozenUser[_account]);
        _;
    }
   
    function addUser (address _userAddress) 
        onlyOwner public {                     3
        users.push(_userAddress);
    }
    
    function refund(address addr) 
        onlyOwner isActive(addr) public {      4
        //...
    }
}

  • 1 Sets the contract owner address at instantiation
  • 2 Modifier definition
  • 3 Associates a modifier with a function in this way
  • 3 A function can have more than one modifier.

From a certain point of view, you can look at a modifier as an implementation of the classic decorator design pattern, as it adds behavior to a function without modifying its logic. As for decorators, you can chain modifiers, and you can attach several of them to a function, as shown in the refund() function, which can execute only if the caller is the contract owner and the user account isn’t frozen:

   function refund(address addr) onlyOwner isActive(addr) public {
        //...
   }
Warning

Modifiers get called in the reverse order from how they’ve been placed on the function definition. In the example, isActive is applied first and onlyOwner second.

5.3.7. Variable declaration, initialization, and assignment

Earlier, I explained how the calling code respectively sets and handles the input and output function parameters. I also illustrated relatively complex cases, such as how you can assign multiple variables from tuple results. In this section, I’ll present more information about the declaration, initialization, and assignment of local function variables. Some of the considerations also apply to state variables.

Implicit initialization

Contrary to most statically typed languages, which force the developer to explicitly initialize variables, when you declare a variable in Solidity, it’s implicitly initialized to its default value, corresponding to its bits being all set to zero, as summarized in table 5.8.

Table 5.8. Default values of solidity types

Type

Default value

Example

int and uint (all sizes) 0 int32 a; //0
bool false bool flag; //false
bytes1 to bytes32 All bytes set to 0 bytes4 byteArray; // 0x00000000
Static array All items set to zero value bool [3] flags; // [false, false, false]
bytes Empty byte array []
Dynamic array Empty array int [] values; // []
string Empty string ""
struct Each element set to the default value
Note

As explained in table 5.8, initialized variables are set to a zero-like value. There is no null value in Solidity.

Delete

You can reinitialize the value of a variable to its default value, as shown in table 5.8, by calling delete on it, as shown in the following code:

contract DeleteExample {
    function deleteExample() returns (int32[5]) {
        int32[5] memory fixedSlots = [int32(5), 9, 1, 3, 4];
        //...
        delete fixedSlots;         1
        return fixedSlots;
    }
}

  • 1 Implicitly reinitializes fixedSlots to [int32 (0), 0, 0, 0, 0]

You can execute this in Remix. Make sure you check the final value of fixedSlots in the output panel on the bottom left, as usual.

Implicitly typed declaration

You can declare the type of a variable implicitly with var if this can be inferred from an explicit initialization, as shown in this code:

contract TaxCalculator {
    function calculateAlpha(int _x) 
        public returns (int _alpha)  {
        var _gammaParams =  [int(5), 9];               1
        var _gamma = calculateGamma(_gammaParams[0], 
          _gammaParams[1]);                            2
        _alpha = _x + _gamma;
    }

    function calculateGamma(int _y, int _z)  
        private returns (int _gamma) {
            _gamma  = _y *3;
    }
}

  • 1 Implicitly declares _gammaParams as int32 [2]
  • 2 Implicitly declares _gamma as int
Note

Implicitly typed variable declaration with var doesn’t mean Solidity supports dynamic typing. It means you can perform the type declaration implicitly rather than explicitly, but still at compile time.

Multiple implicitly typed declarations are also possible when destructuring a tuple returned from a function to multiple variables. For example, given the following calculate() function

contract Calculator {
    function calculate(uint _x) 
        public returns (uint _a, uint _b, 
          bool _ok) {                     1

        //...
        _a = _x * 2;                      2
        _b = _x** 3;                      2

        _ok == (_a * _b) < 10000;         2
    }
}

  • 1 Returns a tuple including three items
  • 2 Sets the tuple items within the function body

it’s possible to destructure the tuple result into three variables, as follows:

var (_alpha, _beta, _success) = 
    calculatorInstance.calculate(5);        1

  • 1 Assigns the tuple returned by calculate() to three variables
Note

Destructuring means decomposing a tuple into its individual constituents, which are then assigned to separate variables.

Tuple assignment

When assigning a tuple to several implicitly or explicitly typed variables, the assignment will work if the number of items in the tuple is at least equal to the number of variables on the left-hand side of the assignment. This code shows examples of correct and incorrect assignments, given the calculate() function defined earlier:

var (_alpha, _beta, ) = 
    calculatorInstance.calculate(5);      1
var (_alpha, _beta, _gamma, _ok) = 
    calculatorInstance.calculate(5);      2

  • 1 Ignores the _ok flag but will be successful
  • 2 Will fail with an error because it’s trying to assign four variables from a three-item tuple

It’s also possible to set various properties of a struct from a tuple. For example, given this struct

struct Factors {
    uint alpha;
    uint beta;
}

it’s possible to set its properties as follows:

var factors = Factors({alpha:0, beta:0}); 
(factors.alpha, factors.beta, ) = 
   calculatorInstance.calculate(5);        1

  • 1 Destructures the tuple result into the properties of a Factors struct object. (Note that, as in the previous example, the _ok flag that calculate() returns has been ignored.)

5.3.8. Events

An event allows a contract to notify another contract or a contract client, such as a Dapp user interface, that something of interest has occurred. You declare events like you do in C# and Java and publish them with the emit keyword, as you can see in the following code extract from SimpleCoin:

pragma solidity ^0.4.16;
contract SimpleCoin {
    mapping (address => uint256) public coinBalance; 
    //...
    event Transfer(address indexed from, 
          address indexed to, uint256 value);           1
    //...

    function transfer(address _to, uint256 _amount) public {
        //...
        coinBalance[msg.sender] -= _amount;  
        coinBalance[_to] += _amount;   
        emit Transfer(msg.sender, _to, _amount);        2
    }
    //...
}

  • 1 Defines the Transfer event
  • 2 Publishes the Transfer event with emit

Events in Ethereum haven’t only a real-time notification purpose, but also a long-term logging purpose. Events are logged on the transaction log of the blockchain, and you can retrieve them later for analysis. To allow quick retrieval, events are indexed against a key that you can define when you declare the event. The key can be composite and contain up to three of its input parameters, as you can see in the definition of Transfer shown previously:

event Transfer(address indexed from, address indexed to, uint256 value);

In chapter 6, you’ll see how to listen and react to Solidity events from client JavaScript code. In chapter 13, you’ll learn more about how events get logged on the blockchain and how you can reply to them and retrieve them.

5.3.9. Conditional statements

Solidity supports all classic conditional statements available in C-like and Java-like languages:

  • if ... else
  • while
  • do ... while
  • for

Loops support both continue and break statements.

You’ve completed the first part of your tour of Solidity. If you want to learn more about the syntax I’ve introduced in this chapter, I encourage you to consult the official documentation at https://solidity.readthedocs.io/en/develop/. In the next section, you’ll apply what you’ve learned in this chapter to improve SimpleCoin. The Solidity tour will then continue in the next chapter, where you’ll start writing code in an object-oriented way and learn about other advanced features of the language.

Warning

Although you might think that because transactions are executed sequentially within the EVM, concurrency issues might not come up within a contract, this isn’t entirely true. A contract might invoke a function on an external contract, and this might lead to concurrency issues, especially if the external contract calls back the caller, as you’ll see in chapter 14 on security.

5.4. Time to improve and refactor SimpleCoin

In this section, you’ll extend SimpleCoin’s functionality as follows:

  • You’ll let the owner of an account authorize an allowance to another account.
  • You’ll restrict certain operations, such as minting coins or freezing accounts, only to the contract owner.

Before making any changes, open Remix and enter the latest version of the SimpleCoin code from chapter 4, as shown in the following listing, into the editor.

Listing 5.7. Latest version of SimpleCoin from chapter 4
pragma solidity ^0.4.0;

contract SimpleCoin {
  mapping (address => uint256) public coinBalance;
    
  event Transfer(address indexed from, address indexed to, uint256 value);

  constructor(uint256 _initialSupply) public {
    coinBalance[msg.sender] = _initialSupply;   
  }

  function transfer(address _to, uint256 _amount) public {
    require(coinBalance[msg.sender] > _amount);
    require(coinBalance[_to] + _amount >= coinBalance[_to] );
    coinBalance[msg.sender] -= _amount;  
    coinBalance[_to] += _amount;   
    emit Transfer(msg.sender, _to, _amount);  
  }
}

Now you can try letting the owner of an account authorize an allowance that another account can use. This means that if account A has 10,000 coins, its owner can authorize account B to transfer a certain amount of coins (say up to a total of 200 in separate transfer operations) to other accounts.

5.4.1. Implementing an allowance facility

You can model a token allowance with a nested mapping:

mapping (address => mapping (address => uint256)) public allowance;

This means that an account allows one or more accounts to manage a specified number of coins; for example:

allowance[address1][address2] = 200;   1
allowance[address1][address3] = 150;   2

  • 1 address2 can manage 200 coins of the address1 balance.
  • 2 address3 can manage 150 coins of the address1 balance.

You can authorize an allowance by calling the following function:

function authorize(address _authorizedAccount, uint256 _allowance) 
    public returns (bool success) {   
    allowance[msg.sender][_authorizedAccount] = 
      _allowance;                                 1
    return true;
}

  • 1 Allows authorizedAccount to manage a number of coins equal to _allowance

Once an account has been authorized an allowance, it can transfer a number of coins, up to the unused allowance, to another account, with the following function:

function transferFrom(address _from, address _to, uint256 _amount) 
    public returns (bool success) { 
    require(_to != 0x0);                         1
    require(coinBalance[_from] > _amount);       2
    require(coinBalance[_to] + 
       _amount >= coinBalance[_to] );            3
    require(_amount <= 
        allowance[_from][msg.sender]);           4

    coinBalance[_from] -= _amount;               5
    coinBalance[_to] += _amount;                 6
    allowance[_from][msg.sender] -= _amount;     7

    emit Transfer(_from, _to, _amount);          8

    return true;
}

  • 1 Prevents transfer to 0x0 address, which is a default address if not specified explicitly
  • 2 Checks if the source account has enough coins
  • 3 Checks for overflow
  • 4 Checks unused allowance
  • 5 Debits source account
  • 6 Increases recipient account
  • 7 Decreases unused allowance
  • 8 Raises Transfer event

The implementation of the allowance facility was relatively simple. Now you can see how to restrict some SimpleCoin functionality only to the contract owner.

5.4.2. Restricting operations only to the contract owner

The contract owner is the account from which the contract gets deployed. SimpleCoin already has an operation that’s executed against the contract owner. As you’ll remember, the constructor assigns the initial token supply to the contract owner, although this assignment is implicit:

constructor(uint256 _initialSupply) {
    coinBalance[msg.sender] = _initialSupply;   
}

You can make the intention of the code more explicit by declaring the contract owner as address public owner; then you can change the constructor to

constructor(uint256 _initialSupply) public {
    owner = msg.sender;                      1
    coinBalance[owner] = _initialSupply;     2
}

  • 1 Initializes the contract owner with the address of the account deploying the contract
  • 2 Assigns the initial token supply explicitly to the contract owner
Minting coins

After you’ve initialized the owner variable, you can restrict the execution of some functions to require that the contract owner invoke them. For example, you could extract the constructor code assigning the initial supply to the owner into a new, more general function:

function mint(address _recipient, uint256  _mintedAmount) public {
    require(msg.sender == owner);                                  1
    coinBalance[_recipient] += _mintedAmount;                      2
    emit Transfer(owner, _recipient, 
       _mintedAmount);
}

  • 1 Restricts the invocation of this function only to the contract owner
  • 2 Assigns the minted amount to the recipient

Then you can change the constructor as follows:

constructor(uint256 _initialSupply) public {
    owner = msg.sender;
    mint(owner, _initialSupply);       1
}

  • 1 The initial supply is now generated through the mint() function.

The mint() function now allows the owner to generate coins at will, not only at construction. The check performed on the first line of mint() makes sure only the owner can generate mint coins.

Note

When looking at the code of token modeling smart contracts, you’ll often find that functions that generate new coins or tokens are named mint(), after the English verb that’s associated with making conventional metallic coins as currency.

Freezing accounts

You might want to further extend the powers of the contract owner and grant them the exclusive ability to freeze accounts. You can model the set of accounts that have been frozen with the following mapping:

mapping (address => bool) public frozenAccount;

The ideal data structure probably would be a Python set (or a C# Set or a Java HashSet), which would allow you to store frozen addresses (the keys of the mapping above) and check them efficiently without having to store any associated value (for example, the Boolean flag in the previous mapping). But a mapping of an address to a Boolean can be considered a close approximation to a set of addresses.

You also can declare an event you can publish when freezing an account:

event FrozenAccount(address target, bool frozen);

The owner would then freeze an account with the following function:

function freezeAccount(address target, bool freeze) public{
    require(msg.sender == owner);                            1

    frozenAccount[target] = freeze;                          2

    emit FrozenAccount(target, freeze);                      3
}

  • 1 Restricts the invocation of this function only to the contract owner
  • 2 Adds the target account to the set of frozen accounts
  • 3 Raises the event
Note

You can use the freezeAccount() function to freeze or unfreeze accounts depending on the value of the Boolean parameter.

You might have noticed that the check being performed on msg.sender, which restricts the caller of this function to only the owner, is exactly the same as what you have on mint(). Wouldn’t it be nice to encapsulate this check in a reusable way? Hold on ... this is exactly the purpose of function modifiers!

Creating the onlyOwner modifier

I know, I know! If you’re among the readers who paid attention to the onlyOwner modifier I presented in listing 5.6, I bet you were wondering with frustration why I hadn’t used it since the beginning of this section. Well, I wanted to show you the usefulness of modifiers the hard way. Now you can refactor the duplicated check of the message sender’s address against the owner’s address into the onlyOwner modifier:

modifier onlyOwner {
    if (msg.sender != owner) revert();      1

    _;
}

  • 1 Won’t allow message callers who aren’t the contract owner to call the modified function.

You can then simplify mint() and freezeAccount() as follows:

function mint(address _recipient, uint256  _mintedAmount) 
    onlyOwner public {                                      1
    coinBalance[_recipient] += _mintedAmount; 
    emit Transfer(owner, _recipient, _mintedAmount); 
}

function freezeAccount(address target, bool freeze) 
    onlyOwner public {                                      1
    frozenAccount[target] = freeze;  
    emit FrozenAccount(target, freeze);
}

  • 1 The onlyOwner modifier replaces the previous check on msg.sender.

You can see the improved SimpleCoin contract, including allowance setting and restricted coin minting and account freezing functionality, in the following listing.

Listing 5.8. Refactored version of SimpleCoin with extended functionality
pragma solidity ^0.4.24;
contract SimpleCoin {
  mapping (address => uint256) public coinBalance;
  mapping (address => mapping (address => uint256)) public allowance;
  mapping (address => bool) public frozenAccount;
  address public owner;
    
  event Transfer(address indexed from, address indexed to, uint256 value);
  event FrozenAccount(address target, bool frozen);
   
  modifier onlyOwner {
    if (msg.sender != owner) revert();
    _;
  }

  constructor(uint256 _initialSupply) public {
    owner = msg.sender;

    mint(owner, _initialSupply);
  }
    
  function transfer(address _to, uint256 _amount) public {
    require(_to != 0x0); 
    require(coinBalance[msg.sender] > _amount);
    require(coinBalance[_to] + _amount >= coinBalance[_to] );
    coinBalance[msg.sender] -= _amount;  
    coinBalance[_to] += _amount;  
 
    emit Transfer(msg.sender, _to, _amount);  
  }
    
  function authorize(address _authorizedAccount, uint256 _allowance) 
    public returns (bool success) {
    allowance[msg.sender][_authorizedAccount] = _allowance; 
    return true;
  }
    
  function transferFrom(address _from, address _to, uint256 _amount) 
    public returns (bool success) {
    require(_to != 0x0);  
    require(coinBalance[_from] > _amount); 
    require(coinBalance[_to] + _amount >= coinBalance[_to] ); 
    require(_amount <= allowance[_from][msg.sender]);  
    
    coinBalance[_from] -= _amount; 
    coinBalance[_to] += _amount; 
    allowance[_from][msg.sender] -= _amount;
    emit Transfer(_from, _to, _amount);
    return true;
  }
    
  function mint(address _recipient, uint256  _mintedAmount) 
    onlyOwner public { 
            
    coinBalance[_recipient] += _mintedAmount; 
    emit Transfer(owner, _recipient, _mintedAmount); 
  }
    
  function freezeAccount(address target, bool freeze) 
    onlyOwner public { 

    frozenAccount[target] = freeze;  
    emit FrozenAccount(target, freeze);
  }
}

You’ve completed the implementation of the proposed improvements. Along the way, you’ve seen a function modifier in action. In the next chapter, which will focus on Solidity’s more advanced object-oriented features, you’ll further improve SimpleCoin’s code.

Summary

  • Various EVM languages have been developed, but Solidity is the most popular one.
  • The structure of a Solidity smart contract is similar to the structure of an object-oriented class.
  • Solidity has two main groups of types: value types, which include enums, primitive types (integer types, bool, address), and functions; and reference types, which include arrays, strings, structs, and mapping.
  • You can store reference type objects in any memory or in storage.
  • You always store state variables in storage, never in memory.
  • Functions are the equivalent of object-oriented methods; they can accept several parameters and can return a single result or several results in a tuple.
  • An unassigned variable is set with a default value depending on its type.
  • You can destructure a tuple returned from a function into separate variables.
  • You can define both state variables and functions with a specific accessibility level: private, internal, public, or external. (The latter only applies to functions.)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset