- references
- https://www.oreilly.com/library/view/hands-on-smart-contract/9781492045250/
- https://www.amazon.com/Solidity-Programming-Essentials-building-contracts/dp/1803231181
- https://www.amazon.com/Beginning-Ethereum-Smart-Contracts-Programming/dp/1484292707
- https://www.springerprofessional.de/en/ethereum-smart-contract-development-in-solidity/18334966
- https://www.manning.com/books/blockchain-in-action
- https://www.packtpub.com/product/mastering-blockchain-programming-with-solidity/9781839218262
- https://www.algoexpert.io/blockchain/index
- https://chat.openai.com
- https://www.investopedia.com/terms/b/basic-attention-token.asp
- https://medium.com/@danielyamagata/understand-evm-opcodes-write-better-smart-contracts-e64f017b619
- https://ethereum.stackexchange.com/questions/117100/why-do-many-solidity-projects-prefer-importing-specific-names-over-whole-modules
- https://medium.com/@eiki1212/what-is-ethereum-gas-simple-explanation-2f0ae62ed69c
- https://ethereum.stackexchange.com/questions/38387/contract-address-transfer-method-gas-cost
- https://medium.com/@natelapinski/the-difference-between-tx-origin-60737d3b3ab5
- https://ethereum.stackexchange.com/questions/21448/how-to-get-a-contracts-balance-in-solidity
- https://medium.com/@blockchain101/solidity-bytecode-and-opcode-basics-672e9b1a88c2
- https://medium.com/coinmonks/solidity-storage-vs-memory-vs-calldata-8c7e8c38bce
- https://ethereum.stackexchange.com/questions/123169/can-calldata-be-used-in-every-function-visibility
- https://ethereum.stackexchange.com/questions/74442/when-should-i-use-calldata-and-when-should-i-use-memory
- https://ethereum.stackexchange.com/questions/117770/how-to-use-calldata-return-values-in-solidity-and-when-are-they-useful
- https://ethereum.stackexchange.com/questions/64108/whats-the-difference-between-address-and-address-payable
- https://docs.soliditylang.org/
- https://medium.com/soonami/3-secure-alternative-methods-to-replace-tx-origin-for-solidity-developers-4acc2c49e1c
- https://ethereum.stackexchange.com/questions/82240/what-is-the-immutable-keyword-in-solidity
- https://coinsbench.com/solidity-layout-and-access-of-storage-variables-simply-explained-1ce964d7c738
- https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/assert-require-revert
- https://stackoverflow.com/questions/71502322/difference-between-assert-and-require
- https://stackoverflow.com/questions/70216805/why-assertion-is-used-on-this-smart-contract
- https://ethereum.stackexchange.com/questions/15166/difference-between-require-and-assert-and-the-difference-between-revert-and-thro
- https://medium.com/coinmonks/error-handling-in-solidity-pt-4-ce2fef14ceb2
- https://medium.com/@solidity101/writing-robust-smart-contracts-with-require-assert-and-revert-in-solidity-a997f6c9b13f
- https://medium.com/coinmonks/solidity-fundamentals-a95bb6c8ba2a
- https://thekscar.medium.com/writing-solidity-unit-tests-for-testing-assert-require-and-revert-conditions-using-truffle-2e182d91a40f
- https://medium.com/coinmonks/learn-solidity-lesson-26-error-handling-ccf350bc9374
- https://ethereum.stackexchange.com/questions/131384/is-msg-sender-reliable-in-view-and-pure-functions
- https://www.alchemy.com/overviews/solidity-gas-optimization
- https://medium.com/coinmonks/ethereum-data-transaction-receipt-trie-and-logs-simplified-30e3ae8dc3cf
- https://medium.com/cryptronics/ethereum-smart-contract-security-73b0ede73fa8
- https://medium.com/what-is-infura/what-is-infura-59dbdd778455
- https://ethereum.stackexchange.com/questions/6897/what-is-the-difference-between-truffle-and-remix
- https://remix-ide.readthedocs.io/en/latest/unittesting.html#customization
- goals of this workshop
- understanding memory model of emv
- introduction to smart contract
- basics of Solidity programming
- introduction to Remix: https://remix.ethereum.org
- workshop task
- implement event ticketing smart contract
createTicket(qrCode)
qrCode
is link to IPFS
buyTicket(ticketId)
verifyTicket(ticketId)
- assume that ticketId is derived from
qrCode
by other system
- assume that ticketId is derived from
getQRCode(ticketId)
- implement tests in solidity
- providing ethers to test
/// #value: 10 function test() public payable { Assert.equal(msg.value, 10, ...); }
- defining sender for the test
import "remix_accounts.sol"; // must be imported in your test file to use custom sender /// #sender: account-0 function test() public payable { Assert.equal(msg.sender, TestsAccounts.getAccount(0), ...); }
- check for exceptions
function test() public { try methodToTest { Assert.ok(false, 'method execution should fail'); } catch Error(string memory reason) { Assert.equal(reason, 'expected reason', 'failed with unexpected reason'); } catch (bytes memory /*lowLevelData*/) { Assert.ok(false, 'failed unexpected'); } }
- providing ethers to test
- implement event ticketing smart contract
- storage
- permanent storage space
- stored on the blockchain
- where all state variables are stored
- each contract account has its own storage and can only access their own storage
- it is not possible to directly access the storage of another account
- each Ethereum account has its own unique address and associated storage
- this address is derived from the account's public key
- as a result, only the account owner has the private key necessary to access or modify the storage
- think of storage as a private database
- thinking of the storage as an array will help us understand it better
- each space in this storage "array" is called a slot and holds
32
bytes of data (256 bits
) - maximum length of this storage "array" is
2²⁵⁶-1
- each slot can be occupied by more than one type
- unless sum of bytes are <= 32
- so order matters
- example
and we can get storage at specific slot
// SPDX-License-Identifier: MIT pragma solidity ^0.8.16; contract StorageLayout { uint64 public value1 = 1; uint64 public value2 = 2; uint64 public value3 = 3; // order matters: you will need 3 slots if value3 and value5 are swapped uint64 public value4 = 4; uint256 public value5 = 5; }
web3.eth.getStorageAt("0x9168fBa74ADA0EB1DA81b8E9AeB88b083b42eBB4", 0) // returns: `0x04000000000000000300000000000000020000000000000001` // so we have value1, ..., value4 in one slot web3.eth.getStorageAt("0x9168fBa74ADA0EB1DA81b8E9AeB88b083b42eBB4", 1) // returns: `0x0000000000000000000000000000000000000000000000000000000000000005`
- dynamic arrays and structs always occupy a new slot
- any variables following them will also be initialized to start a new storage slot
- each space in this storage "array" is called a slot and holds
- it is not cheap in terms of gas - so we need to optimize the use of storage
contract storageExample { uint256 sumOfArray; function inefficientSum(uint256 [] memory _array) public { for(uint256 i; i < _array.length; i++) { sumOfArray += _array[i]; // writing directly to storage } } function efficientSum(uint256 [] memory _array) public { uint256 tempVar; for(uint256 i; i < _array.length; i++) { tempVar += _array[i]; // using temporary memory variable } sumOfArray = tempVar; } }
- Solidity does not have null values
- not assigning a value to a state variable = assigning its default value based on the type
- example
address
->0x0000000000000000000000000000000000000000
- enums -> assigned the first value (index 0)
- permanent storage space
- memory
- works similarly to the memory of a computer, more specifically, the RAM (Random Access Memory)
- idea of RAM is that information can be read and stored in specific places (at a particular memory address) and not just sequentially
- short-lived
- reserved for variables that are defined within the scope of a function
- gets torn down when a function completes its execution
- works similarly to the memory of a computer, more specifically, the RAM (Random Access Memory)
- stack
- works on the LIFO (Last-In First-Out) scheme
- when the bytecode starts executing, the Stack is empty
- 1,024 levels deep in the EVM
- if it stores anything more than this, it raises an exception
- calldata
- it is a read-only location
- example
contract Storage { string[] messages; function retrieve(uint index) public view returns (string calldata){ return messages[index]; // not compiling } }
- for it to work: need to copy string to the calldata area
- impossible: calldata is immutable
- however: if you already have something in calldata though, you can return it
- calldata can be returned from functions
- for it to work: need to copy string to the calldata area
- can typically only be used with functions that have external visibility
- source of these arguments needs to come from message calldata
- example: passing forward calldata arguments
- example
- usually the signature of the function to be executed, followed by the ABI encoding of the function arguments
- can be verified in Remix, under smart contract method you can "Copy calldata to clipboard"
- example
- keccak256 online: https://emn178.github.io/online-tools/keccak_256.html
- function:
function createTicket(string)
6897082f779ee6aa6c305e01892e057838143a4691bda17d4092e228fc6d147a
- selector:
0x6897082f
- argument:
c
0x63
- prepending the data length:
0x0163
- we need to zero-pad to a 32-byte word
- calldata:
concat(selector, packedPaddedArgument)
- temporary location where function arguments are stored
- avoids unnecessary copies and ensures that the data is unaltered
- helps lower gas consumption
- compiler can skip ABI encoding
- the data is already formatted correctly according to the ABI
- for memory: Solidity would need to encode it before returning
- compiler can skip ABI encoding
- calldata is allocated by the caller, while memory is allocated by the callee
- it is a read-only location
- assignments will either result in copies being created, or mere references to the same piece of data
- between storage and memory/calldata - always create a separate copy
- from memory to memory
- create a new copy for value types
- create references for reference types
- changing one memory variable alters all other memory variables that refer to the same data
- from storage to storage
- assign a reference
- infura
- is a kind of node storage (cluster)
- set of tools that provides its services for integrating your application with the Ethereum network
- you do not need to run your local blockchain for the mainnet and testnets
- example: MetaMask internally uses an Infura link to connect to the Ethereum blockchain
- also host the Inter Planetary File System (IPFS) nodes and the IPFS public gateway
- Truffle
- development environment/framework for smart contracts
- can be included in projects as a build dependency
- Remix
- IDE in the browser
- linters
- analyze the given source code and report programming errors, bugs, and stylistic errors
- two commonly used linter tools available
- solhint - provides security and style guideline-specific validations
- ethlint - similar to solhint
- solidity-coverage
- ode coverage tool specifically designed for Solidity smart contracts
- example: https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
- without verification there’s no way to guarantee it matches the code in the blockchain (and we tried a number of combinations)
- in general, code should be posted on Etherscan (not just Github)
- program (bytecode) deployed and executed in the Ethereum Virtual Machine (EVM)
- stored on the Ethereum blockchain
- stores information in blockchain in two distinct ways
- account storage
- contains any data that defines the smart contract state and that the contract can access
- logs
- store information that is not required by the contract but must be accessed by other off chain applications
- example: front-end, analytics etc
- much cheaper than account storage
- store information that is not required by the contract but must be accessed by other off chain applications
- overview
- example: non fungible tokens
- account storage: token Ownership
- contract needs it to prove ownership and provide ownership transfer functionality
- logs
- token ownership history
- contract only needs to be aware of current token ownership
- tracking a token’s ownership history is interest to investors or decision makers
- UI notifications
- transactions are asynchronous, the smart contract cannot return value to the front end
- when the mint occurs, it could write it to the log
- front end could then listen for notifications and display them to the user
- off chain triggers
- when you want to transfer your token to another blockchain
- example: play a game built on a different blockchain
- initiate a transfer to a common gateway, and the transaction will log the transfer
- common gateway would then pick that information and mint a corresponding token in the other chain
- when you want to transfer your token to another blockchain
- token ownership history
- account storage: token Ownership
- account storage
- written in a specific programming language
- example: Solidity, Vyper
- self-executing with the terms of the agreement written directly into code
- automate processes
- Token Sales (ICO)
- can distribute tokens to contributors based on predefined conditions
- Token Sales (ICO)
- enforce rules
- Supply Chain Verification
- can validate products' authenticity based on information stored on the blockchain (e.g., origin, certifications), ensuring compliance with predefined standards
- Supply Chain Verification
- facilitate transactions
- Royalty Payments for Creators
- when revenue is generated from the sale or use of content (e.g., music, art), the smart contract automatically distributes the earnings to the creators according to the agreed-upon terms
- Royalty Payments for Creators
- automate processes
- ability to create DApps = Decentralized Application
- example
- decentralized exchange (DEX): https://uniswap.org
- allows users to trade cryptocurrencies directly with one another without the need for an intermediary or centralized authority
- instead of relying on order books (as in traditional exchanges) uses liquidity pools and smart contracts to facilitate trading
- NFT game: https://www.cryptokitties.co
- decentralized exchange (DEX): https://uniswap.org
- application that runs on a decentralized network of computers (usually a blockchain)
- transactions and data stored in a DApp are recorded on a blockchain
- not controlled by a single entity
- components:
- backend
- smart contract (open sourced)
- user's cryptocurrency wallet
- used to manage and control the user's assets within the DApp
- frontend: GUI user-facing part of the DApp
- responsible for communicating with the smart contracts on the blockchain
- backend
- often have their own native tokens or cryptocurrencies
- used to incentivize network participants and can represent various forms of value
- example: BAT (Basic Attention Token)
- system for tracking media consumers' time and attention on websites using the Brave web browser
- its goal is to efficiently distribute advertising money between advertisers, publishers, and readers of online marketing content and ads
- can operate autonomously without the need for intermediaries
- example
- example
import 'CommonLibrary.sol'; pragma solidity ^0.8.9; contract FirstContract { }
pragma
- generally the first line of code within any Solidity file
- specifies the target compiler version
^
: contract will compile for any version above the version mentioned but less than the next major version- example:
0.8.9
will not work with the0.9.x
compiler version, but lesser than it
- example:
- good practice: compile Solidity code with an exact compiler version rather than using
^
import
- example
import { ERC721HolderUpgradeable // way to avoid naming conflicts } from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol";
- are for your development environment only
- when you deploy your contracts on the Ethereum blockchain, import statements must be replaced with the actual content of the
.sol
file that you imported - Ethereum blockchain takes the flattened files of the smart contract and deploys it
- content of the imported contract is effectively copied and pasted into the current contract during the compilation process
- this means that all elements defined in the imported contract become part of the current contract
- framework, such as Truffle or the Remix IDE, converts all import statements and makes a single flattened contract during deployment
- when you deploy your contracts on the Ethereum blockchain, import statements must be replaced with the actual content of the
- example
- constructors
- are optional and the compiler induces a default constructor when none is explicitly defined
- executed once while deploying the contract
- can have a payable attribute
- enable it to accept Ether during deployment and contract instance creation time
- can be defined as internal
- contract cannot be deployed
- two ways of creating a contract
- using the new keyword
- deploys and creates a new contract instance
- when we deploy the contract, we simply deploy compiled hexadecimals under the data field
- example
is compiled to
pragma solidity ^0.8.21; contract MyContract { }
0x6080604052348015600e575f80fd5b50603e80601a5f395ff3fe60806040525f80fdfea2646970667358221220e48937f3cf7fd35ec1550eb34b94a85d69119e8fe5c9a996d43a65140f5ce75964736f6c63430008150033
- example
- when we deploy the contract, we simply deploy compiled hexadecimals under the data field
- example:
HelloWorld myObj = new HelloWorld();
- deploys and creates a new contract instance
- using the address of the already-deployed contract
- is used when a contract is already deployed and instantiated
- example:
HelloWorld myObj = HelloWorld(address);
- using the new keyword
- this
- represents the current contract
- use case: get balance of current contract
address(this).balance
- inheritance
- C3 linearization / Method Resolution Order (MRO) (similar to Python)
- force a specific order in graphs of base contracts
- contract becomes an abstract contract when it consists of functions without any implementation
- you cannot create an instance of an abstract contract.
- interfaces cannot contain any definitions and any state variables
- only the signature of functions
- C3 linearization / Method Resolution Order (MRO) (similar to Python)
- types
- bool
- uint/int8...256
- example: uint8 - unsigned integer with 8 bits (ranging from 0 to 255)
- uint/int - alias for uint256/int256
- signed integers - hold both negative and positive values
- unsigned integers - hold positive values along with zero
- arrays
- fixed arrays
int[5] age = [int(10), 20, 30, 40, 50]; age[0]; // retrieve
- slot storage
- stored contiguously
- slot storage
- dynamic arrays
int[] age = [int(10), 20, 30, 40, 50]; int[] age = new int[](5); age.push(60); // add age.pop(); // remove age[0]; // retrieve
- slot storage
- in the slot: only its length is saved
- its elements stored somewhere else in the storage
- example
uint256[] public values = [1,2,3,4,5,6,7,8]; uint constant slot = 0 uint constant startingIndexOfValues = keccak256(abi.encode(slot)) function getElementIndexInStorage(uint256 _elementIndex) public pure returns(bytes32) { return bytes32(uint256(startingIndexOfValues) + _elementIndex); }
- elements are stored sequentially from the hash
- space layout applies to them
- example: many elements of
uint8[]
would fit in a single slot until 32 bytes are occupied
- indices are huge and look random
- keccak256 returns a 256 bit number
- storage capacity is 2²⁵⁶-1 elements
- we are good and in range using keccak256 hash as slot index
- makes the probability of 2 or more different state variables sharing the same slot in storage low
- in the slot: only its length is saved
- slot storage
- fixed arrays
- bytes
- bytes1 to bytes32 inclusive
- fixed-size byte array
- example
function test() public pure returns (bytes1) { bytes2 arr = 0x1234; bytes1 first = arr[0]; // 0x12 bytes1 second = arr[0]; // 0x34 return first; }
- String
- do not provide string manipulation methods
- no access by indexed
- no push
- no length property
- to perform any of these - convert into bytes
bytes byteName = bytes(name);
- no equals
Keccak256(''hello world'') == keccak256(''hello world'') // check for equality
- slot storage
- same as fixed arrays
- do not provide string manipulation methods
- Bytes
- similar to dynamic arrays
- address
- one of the most used data types in smart contracts
- provides three functions
- call
- delegatecall
- callcode
- described here: https://github.com/mtumilowicz/solidity-assembly-proxy-workshop
- single property: balance
- designed to hold account addresses in Ethereum
- 160 bits or 20 bytes in size
- cannot be used to send or receive Ethers
- can be converted to
payable address
address addr1 = msg.sender; address payable addr3 = payable(addr1);
- can be converted to
- payable
- superset of the address type
- idea behind this distinction: you are not supposed to send Ether to a plain address
- example: it might be a smart contract that was not built to accept Ether
- idea behind this distinction: you are not supposed to send Ether to a plain address
- additional capability of receiving as well as sending Ether to other accounts
- additional methods used to send ether to a contract or an externally owned account
- transfer/send()
- provides 2,300 units of gas as a fixed limit (cannot be superseded)
- currently only enough to log an event
- low-level functions and should be used with caution
- if used with the contract address, it will invoke a fallback or receive function on the contract
- may recursively call back within the calling contract again and again (reentrancy attack)
- if used with the contract address, it will invoke a fallback or receive function on the contract
send()
function returns Booleantransfer()
raises an exception in the case of execution failure- all changes are reverted
- better alternative to
send()
- provides 2,300 units of gas as a fixed limit (cannot be superseded)
- transfer/send()
- superset of the address type
- mapping
- similar to hash tables or dictionaries in other languages
- example
mapping(address => uint256) public balances; function setBalance(address _address, uint256 _balance) public { balances[_address] = _balance; } function getBalance(address _address) public view returns (uint256) { return balances[_address]; }
- only as a storage type
- not stored sequentially as arrays
- there is no way to order them to save space by fitting smaller types into a single slot
- slot where the mapping is declared does not contain any information
- just empty bytes
- example
and we need to know storage index of the
mapping(address => uint256) public balances;
0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405
- left pad the address to 32 bytes
- left pad the mapping index
- our mapping is declared at index 0 of the storage
- concatenate them and calculate the keccak256 hash for it
- not stored sequentially as arrays
- Solidity does not allow iteration through mapping
OpenZeppelin
providesEnumerableMap
that allows you to create an iterable mapping in Solidity
- possible to have nested mapping
- enum
- example
enum Status { Inactive, Active, Paused } function getStatus() public pure returns (Status) { return Status.Active; }
- values are not directly visible outside the contract
- each enum value is assigned a unique consecutive integer value starting from 0
- if you externally call
getStatus()
you will get int:1
- example
- struct
- help implement custom user-defined data types
- do not contain any logic within them
- slot storage
- slot index where it is declared is reserved for the first value it has
- and then sequentially
- example
Person public p = Person(1, "Jeremy", 28, true);
- id - slot index 0, name - slot index 1 and so on
- slot index where it is declared is reserved for the first value it has
- global variables
- information about the current transaction and blocks
msg
msg.value
- amount of Ether (in wei) sent with the current transaction
msg.gas
- amount of gas remaining in the current transaction
msg.data
- contains the complete calldata
- data payload of the transaction
- contains the function selector and any input data provided when a function is called
- when contract is deployed, constructor selector is used
msg.sig
- first four bytes of the
msg.data
(function selector)- all of the public and external functions have special members available, called selector
- returns the first 4 bytes of the function signature as bytes4
- example
function add(uint256 a, uint256 b) public pure returns (uint256); bytes4 functionSelector = bytes4(keccak256("add(uint256,uint256)")); bytes4 functionSelector = this.add.selector;
- all of the public and external functions have special members available, called selector
- helps to optimize gas usage by directly specifying the function to be executed
- rather than parsing
msg.data
manually
- rather than parsing
- first four bytes of the
msg.sender
- represents the address that is currently calling or interacting with the smart contract
tx.origin
- address of the original sender of the transaction
- EOA that initiated the transaction
- EOAs are the only things in Ethereum that could create a transaction
- can change with ERC-4337 (account abstraction), whereby certain smart contracts will have the ability to issue transactions on behalf of a user
msg.sender
is not always equal totx.origin
- smart contract can call other smart contracts as part of the same transaction
- each time a contract calls another contract, the value of
msg.sender
is updated
- address of the original sender of the transaction
block.timestamp
- timestamp of the current block as a Unix timestamp
- generated by the miners
- no contract should rely on the block timestamp for critical operations
- in particular: should not use it as a seed for random number generation
- Consensys give a 15-seconds rule in their guidelines
- it is safe to use block.timestamp, if your time depending code can deal with a 15 seconds time variation
- cryptographic functions for hashing values
- SHA2 (sha256)
- SHA3 (sha3 or keccak256 function)
- recommended to use the keccak256 function for hashing needs
- is specified in the Ethereum Yellow Paper
- using sha256 could potentially lead to confusion and compatibility issues
- recommended to use the keccak256 function for hashing needs
- qualifiers
- state variables
- internal
- default
- can only be used within current contract functions and any contract that inherits from it
- cannot be directly accessed by external contracts or external actors, its value is still stored on the blockchain and can be observed by inspecting the blockchain's state
- private
- can only be used in contracts containing them
- cannot be directly accessed by external contracts or external actors, its value is still stored on the blockchain and can be observed by inspecting the blockchain's state
- public
- enables external access to state variables
- Solidity compiler generates a getter function for each public state variable
- cannot be directly modified from an externally owned account (EOA) - you need a setter method
- constant
- makes state variables immutable
- the compiler will replace references of this variable everywhere in code with the assigned value
- does not occupy storage on the blockchain
- immutable
- can only be assigned once
- read-only, but assignable in the constructor
- less restricted than those declared as constant
- internal
- functions
- internal - same as for state
- public - same as for state
- private - same as for state
- external
- can only be invoked by other contracts or externally owned accounts (EOAs)
- become part of the contract's interface
- helps to enforce security and transparency in your contract design
- additional qualifiers
- view
- not modify the state of the contract
- state changing actions
- writing to state variables
- emitting events
- creating other contracts
- using selfdestruct
- sending ether via send and transfer
- calling any function not marked view or pure
- using low-level calls
- using inline assembly that contains certain opcodes
- state changing actions
- read-only
- view functions are accessible both off-chain and on-chain
- on-chain
- consumes gas
- example: if function that changes the contract state calls a view function
- off-chain
- doesn't consume any gas
- on-chain
- not modify the state of the contract
- pure
- more restrictive than
view
- cannot access state variables
- more restrictive than
- no way to fully trust
msg.sender
in a view or pure function- signatures are not verified in simple calls
- one way to fully assure yourself of who is calling a function: to have the caller sign and message
and then use
ecrecover
to derive address from signature
- payable
- function can accept Ether only if marked as payable
- view
- state variables
- modifiers
- is always associated with a function
- refers to a construct that changes the behavior of code under execution
- it has the power to change the behavior of functions that it is associated with
- is similar to the decorator pattern in object-oriented programming
- example
modifier onlyOwner() { require(msg.sender == owner, "Only the owner can call this function"); _; // This is a placeholder for the actual function code } function setData(uint256 _value) public onlyOwner { data = _value; }
- events
- transactions can generate events
- used to notify external systems about specific state changes
- example
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); function transferNFT(address from, address to, uint256 tokenId) external { require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized"); _safeTransfer(from, to, tokenId, ""); emit Transfer(from, to, tokenId); }
- example
- stored in a special data structure called the "logs bloom"
- compact representation of all events emitted in a block
- allows nodes to quickly check if a log is included in a block, without having to go through the full log data
- smart contracts cannot hear events on their own because
- contract data lives in the States trie
- event data is stored in the Transaction Receipts trie
- digression: bloom filters
- problem: we want to find out if a user exists in a given list
- naive solution: go through the list
- if a list contains thousands of users, it becomes prohibitively expensive and extremely slow
- probabilistic solution: hashing and mapping each user in the list
- is a probabilistic data structure that can either say “probably present” or “definitely not present”
- is extremely useful, especially when you expect the majority of the answers to be DEFINITELY NOT
- EVM combines the logs bloom of each transaction and creates a logs bloom in the header
- assume we have to search for the same query (tokens sold by a specific user) but across many blocks
- instead of querying the bloom of every transaction in every block, we can simply query the bloom at the header
- assume we have to search for the same query (tokens sold by a specific user) but across many blocks
- up to four indexed parameters (topics)
- used to filter events when querying the blockchain
- first topic is used to log the signature of the event
- the first four bytes of the keccak256 hash of the event's signature
- error handling
- supports try-catch
- possibility to define error objects
- example
error InsufficientBalance(uint available, uint required);
- cannot be used in conjunction with the require function
- must be used in conjunction with the revert function
- example
- revert
- example
if (balanceOf[msg.sender] <= 100) { revert InsufficientBalance(balanceOf[msg.sender], 100); }
- use case
- if it is hard to use
require
- example: complex ifs structures
- if it is hard to use
- example
- require
- using
if (!condition) revert(...)
andrequire(condition, ...)
have the same effect - example
require(msg.value >= price, "Insufficient Ether sent");
- accepts two arguments
- condition that either evaluates to true or false
- optional error message
- string value that is returned to the caller as part of the exception reason
- returns an exception of the Error(string)
- if the evaluation of the statement is false then
- compiles to
0xfd
which is theREVERT
opcode - an exception is raised and execution is halted
- unused gas is returned to the caller
- does not return already-consumed gas
- should be used at the beginning of the function
- state change is reversed
- even if storage variables is made prior to the require statement (within a function)
- compiles to
- use cases - validations
- inputs
- return values
- calls to external contracts
- condition before state update
- correspond to function preconditions in other programming languages
- using
- assert
- used to check for conditions that should never be false
- can't provide a message error
- use case
- test for internal errors
- example: checking the balance of an account after an operation
- check invariants
- example: the token to ether issuance ratio, in a token issuance contract, may be fixed
- verify that this is the case at all times
- example: the token to ether issuance ratio, in a token issuance contract, may be fixed
- when you think that a current state has the potential to become inconsistent
- example: OpenZeppelin’s
- SafeMath.add() function asserts that any summed integers do not overflow
- example: OpenZeppelin’s
- documenting your assumptions with assertions
- commonly employed during the development and testing stages to catch and identify bugs
- test for internal errors
- if the evaluation of the statement is false then
- compiles to
0xfd
which is theREVERT
opcode- uses revert with error signature Panic(uint256)
- till version 0.8.0
- assert(false) compiled to
0xfe
- invalid opcode- using up all remaining gas
- reverting all changes
- assert(false) compiled to
- digression
- non-critical errors revert either with empty error data or Error(string)
- example: division by zero, failing assertions, array access out of bounds
- critical errors revert with Panic(uint256)
- non-critical errors revert either with empty error data or Error(string)
- compiles to
- should often be combined with other techniques, such as pausing the contract and allowing upgrades
- otherwise, you may end up stuck, with an assertion that is always failing
- way to provide a target for formal verification
- example: tools such as the SMTChecker can detect bugs by trying to prove various statements about your code
- based on SMT (Satisfiability Modulo Theories) and Horn solving
- it considers require statements as assumptions and tries to prove that the conditions inside assert statements are always true
- if an assertion failure is found, a counterexample may be given to the user showing how the assertion can be violated
- if no warning is given by for a property, it means that the property is safe
- example: tools such as the SMTChecker can detect bugs by trying to prove various statements about your code