Anatomy of smart contracts
A smart contract is a program that runs at an address on Ethereum. They're made up of data and functions that can execute upon receiving a transaction. Here's an overview of what makes up a smart contract.
Prerequisites
Make sure you've read about smart contracts first. This document assumes you're already familiar with programming languages such as JavaScript or Python.
Data
Any contract data must be assigned to a location: either to storage
or memory
. It's costly to modify storage in a smart contract so you need to consider where your data should live.
Storage
Persistent data is referred to as storage and is represented by state variables. These values get stored permanently on the blockchain. You need to declare the type so that the contract can keep track of how much storage on the blockchain it needs when it compiles.
1// Solidity example2contract SimpleStorage {3 uint storedData; // State variable4 // ...5}Copy
1# Vyper example2storedData: int128Copy
If you've already programmed object-oriented languages, you'll likely be familiar with most types. However address
should be new to you if you're new to Ethereum development.
An address
type can hold an Ethereum address which equates to 20 bytes or 160 bits. It returns in hexadecimal notation with a leading 0x.
Other types include:
- boolean
- integer
- fixed point numbers
- fixed-size byte arrays
- dynamically-sized byte arrays
- Rational and integer literals
- String literals
- Hexadecimal literals
- Enums
For more explanation, take a look at the docs:
Memory
Values that are only stored for the lifetime of a contract function's execution are called memory variables. Since these are not stored permanently on the blockchain, they are much cheaper to use.
Learn more about how the EVM stores data (Storage, Memory, and the Stack) in the Solidity docs.
Environment variables
In addition to the variables you define on your contract, there are some special global variables. They are primarily used to provide information about the blockchain or current transaction.
Examples:
Prop
block.timestamp
State variable
uint256
Description
Current block epoch timestamp
Prop
msg.sender
State variable
address
Description
Sender of the message (current call)
Functions
In the most simplistic terms, functions can get information or set information in response to incoming transactions.
There are two types of function calls:
internal
– these don't create an EVM call- Intenal functions and state variables can only be accessed internally (i.e. from within the current contract or contracts deriving from it)
external
– these do create an EVM call- External functions are part of the contract interface, which means they can be called from other contracts and via transactions. An external function
f
cannot be called internally (i.e.f()
does not work, butthis.f()
works).
- External functions are part of the contract interface, which means they can be called from other contracts and via transactions. An external function
They can also be public
or private
public
functions can be called internally from within the contract or externally via messagesprivate
functions are only visible for the contract they are defined in and not in derived contracts
Both functions and state variables can be made public or private
Here's a function for updating a state variable on a contract:
1// Solidity example2function update_name(string value) public {3 dapp_name = value;4}Copy
- The parameter
value
of typestring
is passed into the function:update_name
- It's declared
public
, meaning anyone can access it - It's not declared
view
, so it can modify the contract state
View functions
These functions promise not to modify the state of the contract's data. Command examples are "getter" functions – you might use this to receive a user's balance for example.
1// Solidity example2function balanceOf(address _owner) public view returns (uint256 _balance) {3 return ownerPizzaCount[_owner];4}Copy
1dappName: public(string)23@view4@public5def readName() -> string:6 return dappNameCopy
What is considered modifying state:
- Writing to state variables.
- Emitting events.
- Creating other contracts.
- Using
selfdestruct
. - Sending ether via calls.
- Calling any function not marked
view
orpure
. - Using low-level calls.
- Using inline assembly that contains certain opcodes.
Constructor functions
constructor
functions are only executed once when the contract is first deployed. Like constructor
in many class-based programming languages, these functions often initialize state variables to their specified values.
1// Solidity example2// Initializes the contract's data, setting the `owner`3// to the address of the contract creator.4constructor() public {5 // All smart contracts rely on external transactions to trigger its functions.6 // `msg` is a global variable that includes relevant data on the given transaction,7 // such as the address of the sender and the ETH value included in the transaction.8 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties9 owner = msg.sender;10}Show allCopy
1# Vyper example23@external4def __init__(_beneficiary: address, _bidding_time: uint256):5 self.beneficiary = _beneficiary6 self.auctionStart = block.timestamp7 self.auctionEnd = self.auctionStart + _bidding_timeCopy
Built-in functions
In addition to the variables and functions you define on your contract, there are some special built-in functions. The most obvious example is:
address.send()
– Soliditysend(address)
– Vyper
These allow contracts to send ETH to other accounts.
Writing functions
Your function needs:
- parameter variable and type (if it accepts parameters)
- declaration of internal/external
- declaration of pure/view/payable
- returns type (if it returns a value)
1pragma solidity >=0.4.0 <=0.6.0;23contract ExampleDapp {4 string dapp_name; //state variable56 /*Called when the contract is deployed and initializes the value*/7 constructor() public{8 dapp_name = "My Example dapp";9 }1011 // Get Function12 function read_name() public view returns(string){13 return dapp_name;14 }1516 // Set Function17 function update_name(string value) public {18 dapp_name = value;19 }20}Show allCopy
A complete contract might look something like this. Here the constructor
function provides an initial value for the dapp_name
variable.
Events and logs
Events let you communicate with your smart contract from your frontend or other subscribing applications. When a transaction is mined, smart contracts can emit events and write logs to the blockchain that the frontend can then process.
Annotated examples
These are some examples written in Solidity. If you'd like to play with the code, you can interact with them in Remix.
Hello world
1// Specifies the version of Solidity, using semantic versioning.2// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma3pragma solidity ^0.5.10;45// Defines a contract named `HelloWorld`.6// A contract is a collection of functions and data (its state).7// Once deployed, a contract resides at a specific address on the Ethereum blockchain.8// Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html9contract HelloWorld {1011 // Declares a state variable `message` of type `string`.12 // State variables are variables whose values are permanently stored in contract storage.13 // The keyword `public` makes variables accessible from outside a contract14 // and creates a function that other contracts or clients can call to access the value.15 string public message;1617 // Similar to many class-based object-oriented languages, a constructor is18 // a special function that is only executed upon contract creation.19 // Constructors are used to initialize the contract's data.20 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors21 constructor(string memory initMessage) public {22 // Accepts a string argument `initMessage` and sets the value23 // into the contract's `message` storage variable).24 message = initMessage;25 }2627 // A public function that accepts a string argument28 // and updates the `message` storage variable.29 function update(string memory newMessage) public {30 message = newMessage;31 }32}Show allCopy
Token
1pragma solidity ^0.5.10;23contract Token {4 // An `address` is comparable to an email address - it's used to identify an account on Ethereum.5 // Addresses can represent a smart contract or an external (user) accounts.6 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#address7 address public owner;89 // A `mapping` is essentially a hash table data structure.10 // This `mapping` assigns an unsigned integer (the token balance) to an address (the token holder).11 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#mapping-types12 mapping (address => uint) public balances;1314 // Events allow for logging of activity on the blockchain.15 // Ethereum clients can listen for events in order to react to contract state changes.16 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#events17 event Transfer(address from, address to, uint amount);1819 // Initializes the contract's data, setting the `owner`20 // to the address of the contract creator.21 constructor() public {22 // All smart contracts rely on external transactions to trigger its functions.23 // `msg` is a global variable that includes relevant data on the given transaction,24 // such as the address of the sender and the ETH value included in the transaction.25 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties26 owner = msg.sender;27 }2829 // Creates an amount of new tokens and sends them to an address.30 function mint(address receiver, uint amount) public {31 // `require` is a control structure used to enforce certain conditions.32 // If a `require` statement evaluates to `false`, an exception is triggered,33 // which reverts all changes made to the state during the current call.34 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions3536 // Only the contract owner can call this function37 require(msg.sender == owner, "You are not the owner.");3839 // Ensures a maximum amount of tokens40 require(amount < 1e60, "Maximum issuance succeeded");4142 // Increases the balance of `receiver` by `amount`43 balances[receiver] += amount;44 }4546 // Sends an amount of existing tokens from any caller to an address.47 function transfer(address receiver, uint amount) public {48 // The sender must have enough tokens to send49 require(amount <= balances[msg.sender], "Insufficient balance.");5051 // Adjusts token balances of the two addresses52 balances[msg.sender] -= amount;53 balances[receiver] += amount;5455 // Emits the event defined earlier56 emit Transfer(msg.sender, receiver, amount);57 }58}Show allCopy
Unique digital asset
1pragma solidity ^0.5.10;23// Imports symbols from other files into the current contract.4// In this case, a series of helper contracts from OpenZeppelin.5// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#importing-other-source-files67import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721.sol";8import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";9import "../node_modules/@openzeppelin/contracts/introspection/ERC165.sol";10import "../node_modules/@openzeppelin/contracts/math/SafeMath.sol";1112// The `is` keyword is used to inherit functions and keywords from external contracts.13// In this case, `CryptoPizza` inherits from the `IERC721` and `ERC165` contracts.14// Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#inheritance15contract CryptoPizza is IERC721, ERC165 {16 // Uses OpenZeppelin's SafeMath library to perform arithmetic operations safely.17 // Learn more: https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath18 using SafeMath for uint256;1920 // Constant state variables in Solidity are similar to other languages21 // but you must assign from an expression which is constant at compile time.22 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constant-state-variables23 uint256 constant dnaDigits = 10;24 uint256 constant dnaModulus = 10 ** dnaDigits;25 bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;2627 // Struct types let you define your own type28 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#structs29 struct Pizza {30 string name;31 uint256 dna;32 }3334 // Creates an empty array of Pizza structs35 Pizza[] public pizzas;3637 // Mapping from owner's address to id of Pizza38 mapping(uint256 => address) public pizzaToOwner;3940 // Mapping from owner's address to number of owned token41 mapping(address => uint256) public ownerPizzaCount;4243 // Mapping from token ID to approved address44 mapping(uint256 => address) pizzaApprovals;4546 // You can nest mappings, this example maps owner to operator approvals47 mapping(address => mapping(address => bool)) private operatorApprovals;4849 // Internal function to create a random Pizza from string (name) and DNA50 function _createPizza(string memory _name, uint256 _dna)51 // The `internal` keyword means this function is only visible52 // within this contract and contracts that derive this contract53 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters54 internal55 // `isUnique` is a function modifier that checks if the pizza already exists56 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers57 isUnique(_name, _dna)58 {59 // Adds Pizza to array of Pizzas and get id60 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);6162 // Checks that Pizza owner is the same as current user63 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions64 assert(pizzaToOwner[id] == address(0));6566 // Maps the Pizza to the owner67 pizzaToOwner[id] = msg.sender;68 ownerPizzaCount[msg.sender] = SafeMath.add(69 ownerPizzaCount[msg.sender],70 171 );72 }7374 // Creates a random Pizza from string (name)75 function createRandomPizza(string memory _name) public {76 uint256 randDna = generateRandomDna(_name, msg.sender);77 _createPizza(_name, randDna);78 }7980 // Generates random DNA from string (name) and address of the owner (creator)81 function generateRandomDna(string memory _str, address _owner)82 public83 // Functions marked as `pure` promise not to read from or modify the state84 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions85 pure86 returns (uint256)87 {88 // Generates random uint from string (name) + address (owner)89 uint256 rand = uint256(keccak256(abi.encodePacked(_str))) +90 uint256(_owner);91 rand = rand % dnaModulus;92 return rand;93 }9495 // Returns array of Pizzas found by owner96 function getPizzasByOwner(address _owner)97 public98 // Functions marked as `view` promise not to modify state99 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions100 view101 returns (uint256[] memory)102 {103 // Uses the `memory` storage location to store values only for the104 // lifecycle of this function call.105 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/introduction-to-smart-contracts.html#storage-memory-and-the-stack106 uint256[] memory result = new uint256[](ownerPizzaCount[_owner]);107 uint256 counter = 0;108 for (uint256 i = 0; i < pizzas.length; i++) {109 if (pizzaToOwner[i] == _owner) {110 result[counter] = i;111 counter++;112 }113 }114 return result;115 }116117 // Transfers Pizza and ownership to other address118 function transferFrom(address _from, address _to, uint256 _pizzaId) public {119 require(_from != address(0) && _to != address(0), "Invalid address.");120 require(_exists(_pizzaId), "Pizza does not exist.");121 require(_from != _to, "Cannot transfer to the same address.");122 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");123124 ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1);125 ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1);126 pizzaToOwner[_pizzaId] = _to;127128 // Emits event defined in the imported IERC721 contract129 emit Transfer(_from, _to, _pizzaId);130 _clearApproval(_to, _pizzaId);131 }132133 /**134 * Safely transfers the ownership of a given token ID to another address135 * If the target address is a contract, it must implement `onERC721Received`,136 * which is called upon a safe transfer, and return the magic value137 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;138 * otherwise, the transfer is reverted.139 */140 function safeTransferFrom(address from, address to, uint256 pizzaId)141 public142 {143 // solium-disable-next-line arg-overflow144 this.safeTransferFrom(from, to, pizzaId, "");145 }146147 /**148 * Safely transfers the ownership of a given token ID to another address149 * If the target address is a contract, it must implement `onERC721Received`,150 * which is called upon a safe transfer, and return the magic value151 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;152 * otherwise, the transfer is reverted.153 */154 function safeTransferFrom(155 address from,156 address to,157 uint256 pizzaId,158 bytes memory _data159 ) public {160 this.transferFrom(from, to, pizzaId);161 require(_checkOnERC721Received(from, to, pizzaId, _data), "Must implmement onERC721Received.");162 }163164 /**165 * Internal function to invoke `onERC721Received` on a target address166 * The call is not executed if the target address is not a contract167 */168 function _checkOnERC721Received(169 address from,170 address to,171 uint256 pizzaId,172 bytes memory _data173 ) internal returns (bool) {174 if (!isContract(to)) {175 return true;176 }177178 bytes4 retval = IERC721Receiver(to).onERC721Received(179 msg.sender,180 from,181 pizzaId,182 _data183 );184 return (retval == _ERC721_RECEIVED);185 }186187 // Burns a Pizza - destroys Token completely188 // The `external` function modifier means this function is189 // part of the contract interface and other contracts can call it190 function burn(uint256 _pizzaId) external {191 require(msg.sender != address(0), "Invalid address.");192 require(_exists(_pizzaId), "Pizza does not exist.");193 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");194195 ownerPizzaCount[msg.sender] = SafeMath.sub(196 ownerPizzaCount[msg.sender],197 1198 );199 pizzaToOwner[_pizzaId] = address(0);200 }201202 // Returns count of Pizzas by address203 function balanceOf(address _owner) public view returns (uint256 _balance) {204 return ownerPizzaCount[_owner];205 }206207 // Returns owner of the Pizza found by id208 function ownerOf(uint256 _pizzaId) public view returns (address _owner) {209 address owner = pizzaToOwner[_pizzaId];210 require(owner != address(0), "Invalid Pizza ID.");211 return owner;212 }213214 // Approves other address to transfer ownership of Pizza215 function approve(address _to, uint256 _pizzaId) public {216 require(msg.sender == pizzaToOwner[_pizzaId], "Must be the Pizza owner.");217 pizzaApprovals[_pizzaId] = _to;218 emit Approval(msg.sender, _to, _pizzaId);219 }220221 // Returns approved address for specific Pizza222 function getApproved(uint256 _pizzaId)223 public224 view225 returns (address operator)226 {227 require(_exists(_pizzaId), "Pizza does not exist.");228 return pizzaApprovals[_pizzaId];229 }230231 /**232 * Private function to clear current approval of a given token ID233 * Reverts if the given address is not indeed the owner of the token234 */235 function _clearApproval(address owner, uint256 _pizzaId) private {236 require(pizzaToOwner[_pizzaId] == owner, "Must be pizza owner.");237 require(_exists(_pizzaId), "Pizza does not exist.");238 if (pizzaApprovals[_pizzaId] != address(0)) {239 pizzaApprovals[_pizzaId] = address(0);240 }241 }242243 /*244 * Sets or unsets the approval of a given operator245 * An operator is allowed to transfer all tokens of the sender on their behalf246 */247 function setApprovalForAll(address to, bool approved) public {248 require(to != msg.sender, "Cannot approve own address");249 operatorApprovals[msg.sender][to] = approved;250 emit ApprovalForAll(msg.sender, to, approved);251 }252253 // Tells whether an operator is approved by a given owner254 function isApprovedForAll(address owner, address operator)255 public256 view257 returns (bool)258 {259 return operatorApprovals[owner][operator];260 }261262 // Takes ownership of Pizza - only for approved users263 function takeOwnership(uint256 _pizzaId) public {264 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");265 address owner = this.ownerOf(_pizzaId);266 this.transferFrom(owner, msg.sender, _pizzaId);267 }268269 // Checks if Pizza exists270 function _exists(uint256 pizzaId) internal view returns (bool) {271 address owner = pizzaToOwner[pizzaId];272 return owner != address(0);273 }274275 // Checks if address is owner or is approved to transfer Pizza276 function _isApprovedOrOwner(address spender, uint256 pizzaId)277 internal278 view279 returns (bool)280 {281 address owner = pizzaToOwner[pizzaId];282 // Disable solium check because of283 // https://github.com/duaraghav8/Solium/issues/175284 // solium-disable-next-line operator-whitespace285 return (spender == owner ||286 this.getApproved(pizzaId) == spender ||287 this.isApprovedForAll(owner, spender));288 }289290 // Check if Pizza is unique and doesn't exist yet291 modifier isUnique(string memory _name, uint256 _dna) {292 bool result = true;293 for (uint256 i = 0; i < pizzas.length; i++) {294 if (295 keccak256(abi.encodePacked(pizzas[i].name)) ==296 keccak256(abi.encodePacked(_name)) &&297 pizzas[i].dna == _dna298 ) {299 result = false;300 }301 }302 require(result, "Pizza with such name already exists.");303 _;304 }305306 // Returns whether the target address is a contract307 function isContract(address account) internal view returns (bool) {308 uint256 size;309 // Currently there is no better way to check if there is a contract in an address310 // than to check the size of the code at that address.311 // See https://ethereum.stackexchange.com/a/14016/36603312 // for more details about how this works.313 // TODO Check this again before the Serenity release, because all addresses will be314 // contracts then.315 // solium-disable-next-line security/no-inline-assembly316 assembly {317 size := extcodesize(account)318 }319 return size > 0;320 }321}Show allCopy
Further reading
Check out Solidity and Vyper's documentation for a more complete overview of smart contracts:
Related topics
Related tutorials
- Downsizing contracts to fight the contract size limit – Some practical tips for reducing the size of your smart contract.
- Logging data from smart contracts with events – An introduction to smart contract events and how you can use them to log data.
- Interact with other contracts from Solidity – How to deploy a smart contract from an existing contract and interact with it.