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.
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.
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
This is roughly equivalent to the following Solidity code:
contract Dummy { function dummy(bytes32 _value) returns (bytes32) { return _value; } }
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.
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:
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.
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.
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.
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.
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); } }
In summary, these are the possible items you can declare:
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 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.
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.
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.
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 encapsulate the logic of a contract, are altered by modifiers, have access to state variables, and can raise the events declared on the contract.
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.
During this initial tour of Solidity, you’ll get a firm foundation in the language by learning about most of its syntax and constructs:
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.
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.
Variables declared as bool can have either a true or false value; for example
bool isComplete = false;
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.
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.
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 }
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
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.
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.
If no size is specified, bytes declares a dynamic size byte array, which is a reference type.
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;
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.
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
If sending Ether with send(), you must have error handling to avoid losing Ether:
if (!destinationAddress.send(10)) revert(); 1
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
You can invoke a function on an external contract with call() as follows:
destinationContractAddress.call("contractName", "functionName"); 1
You can send Ether during the external call as follows:
destinationContractAddress.call.value(10)( "contractName", "functionName"); 1
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
Chapter 15 on security will cover in detail how to invoke transfer(), send(), and call() correctly and how to handle errors safely.
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
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:
A third type of data location is available that you can’t explicitly specify:
The following listing shows various reference type variables declared in different data locations.
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 }
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.
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 }
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 }
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 }
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
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
There are four classes of reference types:
Arrays can be static (of fixed size) or dynamic and are declared and initialized in slightly different ways.
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]; }
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.
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 }
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 }
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 is in fact equivalent to bytes but with no length and push() members. You can initialize it with a string literal:
string name = "Roberto";
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.
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 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
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.
The global namespace is a set of implicitly declared variables and functions that you can reference and use in your contract code directly.
The global namespace provides the following five variables:
Table 5.3 summarizes the functions and properties that global variables expose.
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:
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.
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.
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.
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.
pragma solidity ^0.4.0; contract StateVariablesAccessibility { mapping (address => bool) private frozenAccounts; 1 uint isContractLocked; 2 mapping (address => bool) public tokenBalance; 3 ... }
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 ... }
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.
You can specify function input and output parameters in various ways. Let’s see how.
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.)
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.
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 }
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 }
As for state variables, functions also can be declared with different access levels, as summarized in table 5.5.
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 }
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; } }
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.
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 } }
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.
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; } }
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; } }
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.
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 |
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.
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.
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); } }
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 } }
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
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(); } }
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.
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; } }
You can alter the behavior of functions with function modifiers. Keep reading to see how.
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.
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 //... } }
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 { //... }
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.
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.
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.
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 |
As explained in table 5.8, initialized variables are set to a zero-like value. There is no null value in Solidity.
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; } }
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.
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; } }
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 } }
it’s possible to destructure the tuple result into three variables, as follows:
var (_alpha, _beta, _success) = calculatorInstance.calculate(5); 1
Destructuring means decomposing a tuple into its individual constituents, which are then assigned to separate variables.
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
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
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 } //... }
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.
Solidity supports all classic conditional statements available in C-like and Java-like languages:
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.
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.
In this section, you’ll extend SimpleCoin’s functionality as follows:
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.
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.
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
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; }
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; }
The implementation of the allowance facility was relatively simple. Now you can see how to restrict some SimpleCoin functionality 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 }
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); }
Then you can change the constructor as follows:
constructor(uint256 _initialSupply) public { owner = msg.sender; mint(owner, _initialSupply); 1 }
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.
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.
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 }
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!
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 _; }
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); }
You can see the improved SimpleCoin contract, including allowance setting and restricted coin minting and account freezing functionality, in the following listing.
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.