- Programming with Solidity
- Data Types
- Predefined Global Variables and Functions
- Contract Definition
- Functions
- Contract Constructor and selfdestruct
- Adding a Constructor and selfdestruct to Our Faucet Example
- Function Modifiers
- Contract Inheritance
- Error Handling (assert, require, revert)
- Events
- Calling Other Contracts (send, call, callcode, delegatecall)
Programming with Solidity
In this section, we will look at some of the capabilities of the Solidity language. As we mentioned in [intro_chapter], our first contract example was very simple and also flawed in various ways. We’ll gradually improve it here, while exploring how to use Solidity. This won’t be a comprehensive Solidity tutorial, however, as Solidity is quite complex and rapidly evolving. We’ll cover the basics and give you enough of a foundation to be able to explore the rest on your own. The documentation for Solidity can be found on the project website.
Data Types
First, let’s look at some of the basic data types offered in Solidity:
Boolean (bool)
Boolean value, true or false, with logical operators ! (not), && (and), || (or), == (equal), and != (not equal).
Integer (int, uint)
Signed (int) and unsigned (uint) integers, declared in increments of 8 bits from int8 to uint256. Without a size suffix, 256-bit quantities are used, to match the word size of the EVM.
Fixed point (fixed, ufixed)
Fixed-point numbers, declared with (u
)fixed*M*x*N*
where M is the size in bits (increments of 8 up to 256) and N is the number of decimals after the point (up to 18); e.g., ufixed32x2.
Address
A 20-byte Ethereum address. The address object has many helpful member functions, the main ones being balance (returns the account balance) and transfer
(transfers ether to the account).
Byte array (fixed)
Fixed-size arrays of bytes, declared with bytes1 up to bytes32.
Byte array (dynamic)
Variable-sized arrays of bytes, declared with bytes or string.
Enum
User-defined type for enumerating discrete values: enum NAME {LABEL1, LABEL 2, …}.
Arrays
An array of any type, either fixed or dynamic: uint32[][5] is a fixed-size array of five dynamic arrays of unsigned integers.
Struct
User-defined data containers for grouping variables: struct NAME {TYPE1 VARIABLE1; TYPE2 VARIABLE2; ...}
.
Mapping
Hash lookup tables for key => value pairs: mapping(KEY_TYPE => VALUE_TYPE) NAME.
In addition to these data types, Solidity also offers a variety of value literals that can be used to calculate different units:
Time units
The units seconds, minutes, hours, and days can be used as suffixes, converting to multiples of the base unit seconds.
Ether units
The units wei, finney, szabo, and ether can be used as suffixes, converting to multiples of the base unit wei.
In our Faucet contract example, we used a uint (which is an alias for uint256) for the withdraw_amount variable. We also indirectly used an address variable, which we set with msg.sender. We will use more of these data types in our examples in the rest of this chapter.
Let’s use one of the unit multipliers to improve the readability of our example contract. In the withdraw function we limit the maximum withdrawal, expressing the limit in wei, the base unit of ether:
require(withdraw_amount <= 100000000000000000);
That’s not very easy to read. We can improve our code by using the unit multiplier ether, to express the value in ether instead of wei:
require(withdraw_amount <= 0.1 ether);
Predefined Global Variables and Functions
When a contract is executed in the EVM, it has access to a small set of global objects. These include the block, msg, and tx objects. In addition, Solidity exposes a number of EVM opcodes as predefined functions. In this section we will examine the variables and functions you can access from within a smart contract in Solidity.
Transaction/message call context
The msg object is the transaction call (EOA originated) or message call (contract originated) that launched this contract execution. It contains a number of useful attributes:
msg.sender
We’ve already used this one. It represents the address that initiated this contract call, not necessarily the originating EOA that sent the transaction. If our contract was called directly by an EOA transaction, then this is the address that signed the transaction, but otherwise it will be a contract address.
msg.value
The value of ether sent with this call (in wei).
msg.gas
The amount of gas left in the gas supply of this execution environment. This was deprecated in Solidity v0.4.21 and replaced by the gasleft function.
msg.data
The data payload of this call into our contract.
msg.sig
The first four bytes of the data payload, which is the function selector.
Note | Whenever a contract calls another contract, the values of all the attributes of msg change to reflect the new caller’s information. The only exception to this is the delegatecall function, which runs the code of another contract/library within the original msg context. |
Transaction context
The tx object provides a means of accessing transaction-related information:
tx.gasprice
The gas price in the calling transaction.
tx.origin
The address of the originating EOA for this transaction. WARNING: unsafe!
Block context
The block object contains information about the current block:
block.blockhash(__blockNumber__)
The block hash of the specified block number, up to 256 blocks in the past. Deprecated and replaced with the blockhash function in Solidity v0.4.22.
block.coinbase
The address of the recipient of the current block’s fees and block reward.
block.difficulty
The difficulty (proof of work) of the current block.
block.gaslimit
The maximum amount of gas that can be spent across all transactions included in the current block.
block.number
The current block number (blockchain height).
block.timestamp
The timestamp placed in the current block by the miner (number of seconds since the Unix epoch).
address object
Any address, either passed as an input or cast from a contract object, has a number of attributes and methods:
address.balance
The balance of the address, in wei. For example, the current contract balance is address(this).balance.
address.transfer(__amount__)
Transfers the amount (in wei) to this address, throwing an exception on any error. We used this function in our Faucet example as a method on the msg.sender address, as msg.sender.transfer.
address.send(__amount__)
Similar to transfer, only instead of throwing an exception, it returns false on error. WARNING: always check the return value of send.
address.call(__payload__)
Low-level CALL function—can construct an arbitrary message call with a data payload. Returns false on error. WARNING: unsafe—recipient can (accidentally or maliciously) use up all your gas, causing your contract to halt with an OOG exception; always check the return value of call.
address.callcode(__payload__)
Low-level CALLCODE function, like address(this).call(…) but with this contract’s code replaced with that of address. Returns false on error. WARNING: advanced use only!
address.delegatecall()
Low-level DELEGATECALL function, like callcode(…) but with the full msg context seen by the current contract. Returns false on error. WARNING: advanced use only!
Built-in functions
Other functions worth noting are:
addmod, mulmod
For modulo addition and multiplication. For example, addmod(x,y,k) calculates (x + y) % k.
keccak256, sha256, sha3, ripemd160
Functions to calculate hashes with various standard hash algorithms.
ecrecover
Recovers the address used to sign a message from the signature.
selfdestruct(__recipient_address__)
Deletes the current contract, sending any remaining ether in the account to the recipient address.
this
The address of the currently executing contract account.
Contract Definition
Solidity’s principal data type is contract; our Faucet example simply defines a contract
object. Similar to any object in an object-oriented language, the contract is a container that includes data and methods.
Solidity offers two other object types that are similar to a contract:
interface
An interface definition is structured exactly like a contract, except none of the functions are defined, they are only declared. This type of declaration is often called a stub; it tells you the functions’ arguments and return types without any implementation. An interface specifies the “shape” of a contract; when inherited, each of the functions declared by the interface must be defined by the child.
library
A library contract is one that is meant to be deployed only once and used by other contracts, using the delegatecall method (see address object).
Functions
Within a contract, we define functions that can be called by an EOA transaction or another contract. In our Faucet example, we have two functions: withdraw and the (unnamed) fallback function.
The syntax we use to declare a function in Solidity is as follows:
function FunctionName([parameters]) {public|private|internal|external}
[pure|constant|view|payable] [modifiers] [returns (return types)]
Let’s look at each of these components:
FunctionName
The name of the function, which is used to call the function in a transaction (from an EOA), from another contract, or even from within the same contract. One function in each contract may be defined without a name, in which case it is the fallback function, which is called when no other function is named. The fallback function cannot have any arguments or return anything.
parameters
Following the name, we specify the arguments that must be passed to the function, with their names and types. In our Faucet example we defined uint withdraw_amount as the only argument to the withdraw
function.
The next set of keywords (public, private, internal, external) specify the function’s visibility:
public
Public is the default; such functions can be called by other contracts or EOA transactions, or from within the contract. In our Faucet example, both functions are defined as public.
external
External functions are like public functions, except they cannot be called from within the contract unless explicitly prefixed with the keyword this.
internal
Internal functions are only accessible from within the contract—they cannot be called by another contract or EOA transaction. They can be called by derived contracts (those that inherit this one).
private
Private functions are like internal functions but cannot be called by derived contracts.
Keep in mind that the terms internal and private are somewhat misleading. Any function or data inside a contract is always visible on the public blockchain, meaning that anyone can see the code or data. The keywords described here only affect how and when a function can be called.
The second set of keywords (pure, constant, view, payable) affect the behavior of the function:
constant or view
A function marked as a view promises not to modify any state. The term constant is an alias for view that will be deprecated in a future release. At this time, the compiler does not enforce the view modifier, only producing a warning, but this is expected to become an enforced keyword in v0.5 of Solidity.
pure
A pure function is one that neither reads nor writes any variables in storage. It can only operate on arguments and return data, without reference to any stored data. Pure functions are intended to encourage declarative-style programming without side effects or state.
payable
A payable function is one that can accept incoming payments. Functions not declared as payable will reject incoming payments. There are two exceptions, due to design decisions in the EVM: coinbase payments and SELFDESTRUCT inheritance will be paid even if the fallback function is not declared as payable, but this makes sense because code execution is not part of those payments anyway.
As you can see in our Faucet example, we have one payable function (the fallback function), which is the only function that can receive incoming payments.
Contract Constructor and selfdestruct
There is a special function that is only used once. When a contract is created, it also runs the constructor function if one exists, to initialize the state of the contract. The constructor is run in the same transaction as the contract creation. The constructor function is optional; you’ll notice our Faucet example doesn’t have one.
Constructors can be specified in two ways. Up to and including in Solidity v0.4.21, the constructor is a function whose name matches the name of the contract, as you can see here:
contract MEContract {
function MEContract() {
// This is the constructor
}
}
The difficulty with this format is that if the contract name is changed and the constructor function name is not changed, it is no longer a constructor. Likewise, if there is an accidental typo in the naming of the contract and/or constructor, the function is again no longer a constructor. This can cause some pretty nasty, unexpected, and difficult-to-find bugs. Imagine for example if the constructor is setting the owner of the contract for purposes of control. If the function is not actually the constructor because of a naming error, not only will the owner be left unset at the time of contract creation, but the function may also be deployed as a permanent and “callable” part of the contract, like a normal function, allowing any third party to hijack the contract and become the “owner” after contract creation.
To address the potential problems with constructor functions being based on having an identical name as the contract, Solidity v0.4.22 introduces a constructor keyword that operates like a constructor function but does not have a name. Renaming the contract does not affect the constructor at all. Also, it is easier to identify which function is the constructor. It looks like this:
pragma ^0.4.22
contract MEContract {
constructor () {
// This is the constructor
}
}
To summarize, a contract’s life cycle starts with a creation transaction from an EOA or contract account. If there is a constructor, it is executed as part of contract creation, to initialize the state of the contract as it is being created, and is then discarded.
The other end of the contract’s life cycle is contract destruction. Contracts are destroyed by a special EVM opcode called SELFDESTRUCT. It used to be called SUICIDE
, but that name was deprecated due to the negative associations of the word. In Solidity, this opcode is exposed as a high-level built-in function called selfdestruct, which takes one argument: the address to receive any ether balance remaining in the contract account. It looks like this:
selfdestruct(address recipient);
Note that you must explicitly add this command to your contract if you want it to be deletable—this is the only way a contract can be deleted, and it is not present by default. In this way, users of a contract who might rely on a contract being there forever can be certain that a contract can’t be deleted if it doesn’t contain a SELFDESTRUCT
opcode.
Adding a Constructor and selfdestruct to Our Faucet Example
The Faucet example contract we introduced in [intro_chapter] does not have any constructor or selfdestruct functions. It is an eternal contract that cannot be deleted. Let’s change that, by adding a constructor and selfdestruct function. We probably want selfdestruct to be callable only by the EOA that originally created the contract. By convention, this is usually stored in an address variable called owner. Our constructor sets the owner variable, and the selfdestruct function will first check that the owner called it directly.
First, our constructor:
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.22;
// Our first contract is a faucet!
contract Faucet {
address owner;
// Initialize Faucet contract: set owner
constructor() {
owner = msg.sender;
}
[...]
We’ve changed the pragma directive to specify v0.4.22 as the minimum version for this example, as we are using the new constructor keyword introduced in v0.4.22 of Solidity. Our contract now has an address type variable named owner. The name “owner” is not special in any way. We could call this address variable “potato” and still use it the same way. The name owner simply makes its purpose clear.
Next, our constructor, which runs as part of the contract creation transaction, assigns the address from msg.sender to the owner variable. We used msg.sender in the withdraw
function to identify the initiator of the withdrawal request. In the constructor, however, the msg.sender is the EOA or contract address that initiated contract creation. We know this is the case because this is a constructor function: it only runs once, during contract creation.
Now we can add a function to destroy the contract. We need to make sure that only the owner can run this function, so we will use a require statement to control access. Here’s how it will look:
// Contract destructor
function destroy() public {
require(msg.sender == owner);
selfdestruct(owner);
}
If anyone calls this destroy function from an address other than owner, it will fail. But if the same address stored in owner by the constructor calls it, the contract will self-destruct and send any remaining balance to the owner address. Note that we did not use the unsafe tx.origin to determine whether the owner wished to destroy the contract—using tx.origin would allow malign contracts to destroy your contract without your permission.
Function Modifiers
Solidity offers a special type of function called a function modifier. You apply modifiers to functions by adding the modifier name in the function declaration. Modifiers are most often used to create conditions that apply to many functions within a contract. We have an access control statement already, in our destroy function. Let’s create a function modifier that expresses that condition:
modifier onlyOwner {
require(msg.sender == owner);
_;
}
This function modifier, named onlyOwner, sets a condition on any function that it modifies, requiring that the address stored as the owner of the contract is the same as the address of the transaction’s msg.sender. This is the basic design pattern for access control, allowing only the owner of a contract to execute any function that has the onlyOwner modifier.
You may have noticed that our function modifier has a peculiar syntactic “placeholder” in it, an underscore followed by a semicolon (_;). This placeholder is replaced by the code of the function that is being modified. Essentially, the modifier is “wrapped around” the modified function, placing its code in the location identified by the underscore character.
To apply a modifier, you add its name to the function declaration. More than one modifier can be applied to a function; they are applied in the sequence they are declared, as a comma-separated list.
Let’s rewrite our destroy function to use the onlyOwner modifier:
function destroy() public onlyOwner {
selfdestruct(owner);
}
The function modifier’s name (onlyOwner) is after the keyword public and tells us that the destroy function is modified by the onlyOwner modifier. Essentially, you can read this as “Only the owner can destroy this contract.” In practice, the resulting code is equivalent to “wrapping” the code from onlyOwner around destroy.
Function modifiers are an extremely useful tool because they allow us to write preconditions for functions and apply them consistently, making the code easier to read and, as a result, easier to audit for security. They are most often used for access control, but they are quite versatile and can be used for a variety of other purposes.
Inside a modifier, you can access all the values (variables and arguments) visible to the modified function. In this case, we can access the owner variable, which is declared within the contract. However, the inverse is not true: you cannot access any of the modifier’s variables inside the modified function.
Contract Inheritance
Solidity’s contract object supports inheritance, which is a mechanism for extending a base contract with additional functionality. To use inheritance, specify a parent contract with the keyword is:
contract Child is Parent {
...
}
With this construct, the Child contract inherits all the methods, functionality, and variables of Parent. Solidity also supports multiple inheritance, which can be specified by comma-separated contract names after the keyword is:
contract Child is Parent1, Parent2 {
...
}
Contract inheritance allows us to write our contracts in such a way as to achieve modularity, extensibility, and reuse. We start with contracts that are simple and implement the most generic capabilities, then extend them by inheriting those capabilities in more specialized contracts.
In our Faucet contract, we introduced the constructor and destructor, together with access control for an owner, assigned on construction. Those capabilities are quite generic: many contracts will have them. We can define them as generic contracts, then use inheritance to extend them to the Faucet contract.
We start by defining a base contract owned, which has an owner variable, setting it in the contract’s constructor:
contract owned {
address owner;
// Contract constructor: set owner
constructor() {
owner = msg.sender;
}
// Access control modifier
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
Next, we define a base contract mortal, which inherits owned:
contract mortal is owned {
// Contract destructor
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
As you can see, the mortal contract can use the onlyOwner function modifier, defined in owned. It indirectly also uses the owner address variable and the constructor defined in owned. Inheritance makes each contract simpler and focused on its specific functionality, allowing us to manage the details in a modular way.
Now we can further extend the owned contract, inheriting its capabilities in Faucet:
contract Faucet is mortal {
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
// Limit withdrawal amount
require(withdraw_amount <= 0.1 ether);
// Send the amount to the address that requested it
msg.sender.transfer(withdraw_amount);
}
// Accept any incoming amount
function () external payable {}
}
By inheriting mortal, which in turn inherits owned, the Faucet contract now has the constructor and destroy functions, and a defined owner. The functionality is the same as when those functions were within Faucet, but now we can reuse those functions in other contracts without writing them again. Code reuse and modularity make our code cleaner, easier to read, and easier to audit.
Error Handling (assert, require, revert)
A contract call can terminate and return an error. Error handling in Solidity is handled by four functions: assert, require, revert, and throw (now deprecated).
When a contract terminates with an error, all the state changes (changes to variables, balances, etc.) are reverted, all the way up the chain of contract calls if more than one contract was called. This ensures that transactions are atomic, meaning they either complete successfully or have no effect on state and are reverted entirely.
The assert and require functions operate in the same way, evaluating a condition and stopping execution with an error if the condition is false. By convention, assert is used when the outcome is expected to be true, meaning that we use assert to test internal conditions. By comparison, require is used when testing inputs (such as function arguments or transaction fields), setting our expectations for those conditions.
We’ve used require in our function modifier onlyOwner, to test that the message sender is the owner of the contract:
require(msg.sender == owner);
The require function acts as a gate condition, preventing execution of the rest of the function and producing an error if it is not satisfied.
As of Solidity v0.4.22, require can also include a helpful text message that can be used to show the reason for the error. The error message is recorded in the transaction log. So, we can improve our code by adding an error message in our require function:
require(msg.sender == owner, "Only the contract owner can call this function");
The revert and throw functions halt the execution of the contract and revert any state changes. The throw function is obsolete and will be removed in future versions of Solidity; you should use revert instead. The revert function can also take an error message as the only argument, which is recorded in the transaction log.
Certain conditions in a contract will generate errors regardless of whether we explicitly check for them. For example, in our Faucet contract, we don’t check whether there is enough ether to satisfy a withdrawal request. That’s because the transfer function will fail with an error and revert the transaction if there is insufficient balance to make the transfer:
msg.sender.transfer(withdraw_amount);
However, it might be better to check explicitly and provide a clear error message on failure. We can do that by adding a require statement before the transfer:
require(this.balance >= withdraw_amount,
"Insufficient balance in faucet for withdrawal request");
msg.sender.transfer(withdraw_amount);
Additional error-checking code like this will increase gas consumption slightly, but it offers better error reporting than if omitted. You will need to find the right balance between gas consumption and verbose error checking based on the expected use of your contract. In the case of a Faucet contract intended for a testnet, we’d probably err on the side of extra reporting even if it costs more gas. Perhaps for a mainnet contract we’d choose to be frugal with our gas usage instead.
Events
When a transaction completes (successfully or not), it produces a transaction receipt, as we will see in [evm_chapter]. The transaction receipt contains log entries that provide information about the actions that occurred during the execution of the transaction. Events are the Solidity high-level objects that are used to construct these logs.
Events are especially useful for light clients and DApp services, which can “watch” for specific events and report them to the user interface, or make a change in the state of the application to reflect an event in an underlying contract.
Event objects take arguments that are serialized and recorded in the transaction logs, in the blockchain. You can supply the keyword indexed before an argument, to make the value part of an indexed table (hash table) that can be searched or filtered by an application.
We have not added any events in our Faucet example so far, so let’s do that. We will add two events, one to log any withdrawals and one to log any deposits. We will call these events Withdrawal and Deposit, respectively. First, we define the events in the Faucet contract:
contract Faucet is mortal {
event Withdrawal(address indexed to, uint amount);
event Deposit(address indexed from, uint amount);
[...]
}
We’ve chosen to make the addresses indexed, to allow searching and filtering in any user interface built to access our Faucet.
Next, we use the emit keyword to incorporate the event data in the transaction logs:
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
[...]
msg.sender.transfer(withdraw_amount);
emit Withdrawal(msg.sender, withdraw_amount);
}
// Accept any incoming amount
function () external payable {
emit Deposit(msg.sender, msg.value);
}
The resulting Faucet.sol contract looks like Faucet8.sol: Revised Faucet contract, with events.
Example 3. Faucet8.sol: Revised Faucet contract, with events
link:code/Solidity/Faucet8.sol[]
Catching events
OK, so we’ve set up our contract to emit events. How do we see the results of a transaction and “catch” the events? The web3.js library provides a data structure that contains a transaction’s logs. Within those we can see the events generated by the transaction.
Let’s use truffle to run a test transaction on the revised Faucet contract. Follow the instructions in [truffle] to set up a project directory and compile the Faucet
code. The source code can be found in the book’s GitHub repository under code/truffle/FaucetEvents.
$ truffle develop
truffle(develop)> compile
truffle(develop)> migrate
Using network 'develop'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xb77ceae7c3f5afb7fbe3a6c5974d352aa844f53f955ee7d707ef6f3f8e6b4e61
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Faucet...
... 0xfa850d754314c3fb83f43ca1fa6ee20bc9652d891c00a2f63fd43ab5bfb0d781
Faucet: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
truffle(develop)> Faucet.deployed().then(i => {FaucetDeployed = i})
truffle(develop)> FaucetDeployed.send(web3.utils.toWei(1, "ether")).then(res => \
{ console.log(res.logs[0].event, res.logs[0].args) })
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
truffle(develop)> FaucetDeployed.withdraw(web3.utils.toWei(0.1, "ether")).then(res => \
{ console.log(res.logs[0].event, res.logs[0].args) })
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }
After deploying the contract using the deployed function, we execute two transactions. The first transaction is a deposit (using send), which emits a Deposit event in the transaction logs:
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
Next, we use the withdraw function to make a withdrawal. This emits a Withdrawal event:
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }
To get these events, we looked at the logs array returned as a result (res) of the transactions. The first log entry (logs[0]) contains an event name in logs[0].event and the event arguments in logs[0].args. By showing these on the console, we can see the emitted event name and the event arguments.
Events are a very useful mechanism, not only for intra-contract communication, but also for debugging during development.
Calling Other Contracts (send, call, callcode, delegatecall)
Calling other contracts from within your contract is a very useful but potentially dangerous operation. We’ll examine the various ways you can achieve this and evaluate the risks of each method. In short, the risks arise from the fact that you may not know much about a contract you are calling into or that is calling into your contract. When writing smart contracts, you must keep in mind that while you may mostly expect to be dealing with EOAs, there is nothing to stop arbitrarily complex and perhaps malign contracts from calling into and being called by your code.
Creating a new instance
The safest way to call another contract is if you create that other contract yourself. That way, you are certain of its interfaces and behavior. To do this, you can simply instantiate it, using the keyword new, as in other object-oriented languages. In Solidity, the keyword new will create the contract on the blockchain and return an object that you can use to reference it. Let’s say you want to create and call a Faucet contract from within another contract called Token:
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = new Faucet();
}
}
This mechanism for contract construction ensures that you know the exact type of the contract and its interface. The contract Faucet must be defined within the scope of Token, which you can do with an import statement if the definition is in another file:
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = new Faucet();
}
}
You can optionally specify the value of ether transfer on creation, and pass arguments to the new contract’s constructor:
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = (new Faucet).value(0.5 ether)();
}
}
You can also then call the Faucet functions. In this example, we call the destroy function of Faucet from within the destroy function of Token:
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = (new Faucet).value(0.5 ether)();
}
function destroy() ownerOnly {
_faucet.destroy();
}
}
Note that while you are the owner of the Token contract, the Token contract itself owns the new Faucet contract, so only the Token contract can destroy it.
Addressing an existing instance
Another way you can call a contract is by casting the address of an existing instance of the contract. With this method, you apply a known interface to an existing instance. It is therefore critically important that you know, for sure, that the instance you are addressing is in fact of the type you assume. Let’s look at an example:
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor(address _f) {
_faucet = Faucet(_f);
_faucet.withdraw(0.1 ether)
}
}
Here, we take an address provided as an argument to the constructor, _f, and we cast it to a Faucet object. This is much riskier than the previous mechanism, because we don’t know for sure whether that address actually is a Faucet object. When we call withdraw, we are assuming that it accepts the same arguments and executes the same code as our Faucet declaration, but we can’t be sure. For all we know, the withdraw function at this address could execute something completely different from what we expect, even if it is named the same. Using addresses passed as input and casting them into specific objects is therefore much more dangerous than creating the contract yourself.
Raw call, delegatecall
Solidity offers some even more “low-level” functions for calling other contracts. These correspond directly to EVM opcodes of the same name and allow us to construct a contract-to-contract call manually. As such, they represent the most flexible and the most dangerous mechanisms for calling other contracts.
Here’s the same example, using a call method:
contract Token is mortal {
constructor(address _faucet) {
_faucet.call("withdraw", 0.1 ether);
}
}
As you can see, this type of call is a blind call into a function, very much like constructing a raw transaction, only from within a contract’s context. It can expose your contract to a number of security risks, most importantly reentrancy, which we will discuss in more detail in [reentrancy_security]. The call function will return false if there is a problem, so you can evaluate the return value for error handling:
contract Token is mortal {
constructor(address _faucet) {
if !(_faucet.call("withdraw", 0.1 ether)) {
revert("Withdrawal from faucet failed");
}
}
}
Another variant of call is delegatecall, which replaced the more dangerous callcode. The callcode
method will be deprecated soon, so it should not be used.
As mentioned in address object, a delegatecall is different from a call in that the msg context does not change. For example, whereas a call changes the value of msg.sender to be the calling contract, a delegatecall keeps the same msg.sender as in the calling contract. Essentially, delegatecall runs the code of another contract inside the context of the execution of the current contract. It is most often used to invoke code from a library. It also allows you to draw on the pattern of using library functions stored elsewhere, but have that code work with the storage data of your contract.
The delegate call should be used with great caution. It can have some unexpected effects, especially if the contract you call was not designed as a library.
Let’s use an example contract to demonstrate the various call semantics used by call and delegatecall for calling libraries and contracts. In CallExamples.sol: An example of different call semantics, we use an event to log the details of each call and see how the calling context changes depending on the call type.
Example 4. CallExamples.sol: An example of different call semantics
link:code/truffle/CallExamples/contracts/CallExamples.sol[]
As you can see in this example, our main contract is caller, which calls a library calledLibrary and a contract calledContract. Both the called library and the contract have identical calledFunction functions, which emit an event callEvent. The event callEvent logs three pieces of data: msg.sender, tx.origin, and this. Each time calledFunction is called it may have a different execution context (with different values for potentially all the context variables), depending on whether it is called directly or through delegatecall.
In caller, we first call the contract and library directly, by invoking calledFunction in each. Then, we explicitly use the low-level functions call and delegatecall to call calledContract.calledFunction. This way we can see how the various calling mechanisms behave.
Let’s run this in a Truffle development environment and capture the events, to see how it looks:
truffle(develop)> migrate
Using network 'develop'.
[...]
Saving artifacts...
truffle(develop)> web3.eth.accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'
truffle(develop)> caller.address
'0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
truffle(develop)> calledContract.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'
truffle(develop)> calledLibrary.address
'0xf25186b5081ff5ce73482ad761db0eb0d25abfbf'
truffle(develop)> caller.deployed().then( i => { callerDeployed = i })
truffle(develop)> callerDeployed.make_calls(calledContract.address).then(res => \
{ res.logs.forEach( log => { console.log(log.args) })})
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
Let’s see what happened here. We called the make_calls function and passed the address of calledContract, then caught the four events emitted by each of the different calls. Let’s look at the make_calls function and walk through each step.
The first call is:
_calledContract.calledFunction();
Here, we’re calling calledContract.calledFunction directly, using the high-level ABI for calledFunction. The event emitted is:
sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10'
As you can see, msg.sender is the address of the caller contract. The tx.origin is the address of our account, web3.eth.accounts[0], that sent the transaction to caller. The event was emitted by calledContract, as we can see from the last argument in the event.
The next call in make_calls is to the library:
calledLibrary.calledFunction();
It looks identical to how we called the contract, but behaves very differently. Let’s look at the second event emitted:
sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
This time, the msg.sender is not the address of caller. Instead, it is the address of our account, and is the same as the transaction origin. That’s because when you call a library, the call is always delegatecall and runs within the context of the caller. So, when the calledLibrary code was running, it inherited the execution context of caller, as if its code was running inside caller. The variable this (shown as from in the event emitted) is the address of caller, even though it is accessed from within calledLibrary
.
The next two calls, using the low-level call and delegatecall, verify our expectations, emitting events that mirror what we just saw.