Git Product home page Git Product logo

solidity-token-design-patterns-workshop's Introduction

solidity-token-design-patterns-workshop

preface

  • it may be worthwile to take a look here: https://github.com/mtumilowicz/solidity-basics-workshop
  • goals of this workshop
    • know best practices when developing smart contract with Solidity
    • understand some basic attacks & how to protect against them
    • introduction to tokens
      • erc20, erc721, erc1155
    • enumerating some standard design patterns in Solidity
    • understanding basic of IPFS
    • introduction to OpenZeppelin
  • workshop task
    1. implement CustomNftToken - no third party libraries
      1. no third party libraries
        • mint - everyone, but cost 1 eth
        • transfer - only owner of token can transfer it
        • withdraw - only owner of contract can get money
        • implement tests in Solidity
      2. rewrite it using Ownable from OpenZeppelin
    2. OpenZeppelin
      1. implement Erc721Token
        • should define miner role and restrict minting only to miners
        • should define admin role and restrict burning only to admins
        • uri should be configurable per every token (ipfs case)
      2. implement Erc20Token
      3. implement Erc1155Token
        • fungible: GOLD, SILVER, SWORD, SHIELD
        • non-fungible: THOR_HAMMER

best practices

* don't use plain secret on-chain
    * problem: front-running attacks
        * all the transaction data is open and can be seen by others
            * even data of pending transaction can be seen by others
        * example: domain name registration
            * user is registering a unique value
            * attacker watch for the transactions on that contract
                * send the high gas-price transaction to front run the user's transaction
    * solution: commit-and-reveal scheme
        * hash of the original secret is submitted to the blockchain
        * steps
            1. all parties submit their secret hash
            1. all parties reveal their choice by submitting salt (that was used to generate the secret hash)
* don't use `tx.origin` for authorization
    * problem: intercepting transaction
        1. we have `Vault` contract, that has function `withdraw()` using `tx.origin` for authorization
        1. attacker deploys AttackerContract
        1. attacker ask the original owner of the Vault contract to send some ether to the AttackerContract
        1. AttackerContract calls the Vault.withdraw() function
    * solution: always use `msg.sender`
* avoid dependency on untrusted external calls
    * problems
        * if the target contract is killed via selfdestruct, the external call to the function will always fail
        * reentrancy attack
            * re-enter origin contract before the state changes are finalized
        * unpredictable gas costs
* reentrancy attacks
    * problem: re-enter origin contract before the state changes are finalized
        * example (2016 dao hack)
            ```
            function withdraw(uint amount) public {
                 require(balanceOf[msg.sender] >= amount);
                 msg.sender.call{value: amount}(""); // invokes fallback function in caller, which in turns invokes withdraw again
                 balanceOf[msg.sender] -= amount;
                 Withdrawal(msg.sender, amount);
            }
            ```
    * solution: checks-effects-interactions (CEI) pattern
        * example
            ```
            function withdraw(uint amount) public {
                // Checks phase
                require(balanceOf[msg.sender] >= amount, "Insufficient balance");

                // Effects phase
                balanceOf[msg.sender] -= amount;

                // Interactions phase
                (bool success, ) = msg.sender.call{value: amount}("");
                require(success, "Transfer failed");

                // Emit event after successful interaction
                emit Withdrawal(msg.sender, amount);
            }
            ```
* replay attack
    * problem
        * cross-chain signature replay
            * example
                ```
                function deposit() public payable { // reply transaction from testnet on the mainnet
                    balances[msg.sender] += msg.value;
                }
                ```
            * usually happen during chain splits or hard forks
                * Ethereum: signed transaction is valid for all Ethereum chains
                * Bitcoin: addresses in testnet use a different prefix from addresses in mainnet
                    * keys are different
            * solution: use chainId when generating a signature
        * same-chain signature replay
            * example
                ```
                function deposit() public payable { // attacker rebroadcasts the same transaction with the same parameters
                    balances[msg.sender] += msg.value;
                }
                ```
            * solution: use nonce when generating a signature
    * solution
        * two approaches
            * strong replay protection
                * one of the forked chains will make it mandatory to change some information in the transaction for it to be valid over its network
            * opt-in reply protection
                * users must make manual changes to the transaction to ensure that they won’t be replayed
                * example: Ethereum Classic (ETC) did not implement strong replay protection during the hard fork
        * EIP155
            * called "simple replay attack protection"
            * defines the chainID field in Ethereum transactions to prevent replay attacks
            * before EIP155
                * there are 6 inputs to an Ethereum transaction
                    * nonce, gasPrice, gasLimit, to, value, data
                * transaction is not chain specific
                    * same addresses in different networks => can lead to unintended transactions
            * user should sign the data along with a unique nonce value each time
                * example
                    ```
                    mapping(address => uint256) public nonces;

                    function deposit(uint256 nonce) public payable {
                        require(nonce > nonces[msg.sender], "Invalid nonce");
                        nonces[msg.sender] = nonce;
                        balances[msg.sender] += msg.value;
                    }
                    ```
            * every transaction signature should also encapsulate a unique identifier for the specific network
        * EIP191
            * called "signature data standard"
            * introduces a prefix to the data that is being signed
                * example
                    ```
                    "\x19Ethereum Signed Message:\n32"
                    ```
                    * byte `0x19` standardized because an already existing implementation (in the Go Ethereum client software)
                    was using it before the standard was finalized
                    * last number `32` is the byte length of the message (excluding the prefix)
            * reason for prefixing is so that a cleverly designed message cannot possibly be a valid transaction
                * allowing signing raw messages, without a prefix, enables an app to steal all ether, tokens and assets
                    * purpose is entirely to invalidate any payload as a valid RLP encoded transaction
                    * MetaMask does not permit you to perform this operation
                        * it will always force prefixing a signed message even when the message is a hash
                * when you create a transaction: unsigned transaction -> hash it -> sign it
                * when you create a message: unsigned message -> prefix it -> hash it -> sign it
                * example
                    ```
                    let unsignedTransaction = "0xe980850218711a00825208948ba1f109551bd432803012645ac136ddd64dba72880de0b6b3a764000080";
                    ```
                    decoded with https://flightwallet.github.io/decode-eth-tx/
                    ```
                    {
                      "nonce": 0,
                      "gasPrice": 9000000000,
                      "gasLimit": 21000,
                      "to": "0x8ba1f109551bd432803012645ac136ddd64dba72",
                      "value": 1000000000000000000,
                      "data": ""
                    }
                    ```
                    * if attacked gives you this hash and you sign it => it is now a valid signed transaction which will send 1 ether to attacker
                    * string that begins with "\x19Ethereum Signed Message:" is not a valid transaction
                        * it is safe to sign it
            * eliminates the risk of replay attacks on other EVM platforms
                * if there were no platform-specific prefixes, the resulting signature would be the same for all platforms
            * use case
                * smart contract needs to verify a signed message
                * external systems need to interact with Ethereum transactions in a standardized way
            * list of chain ids: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md?ref=blog.hook.xyz#list-of-chain-ids
        * EIP712
            * is a more advanced and secure method of signing a transaction
            * provides a way to hash and sign typed structs rather than just strings
                * uses the keccak256 hashing algorithm
            * requires TypedData (JSON object input)
                * example
                    ```
                    typedData := apitypes.TypedData{
                    	Types:       types,
                    	PrimaryType: "ERC721Order",
                    	Domain:      domain,
                    	Message:     message,
                    }
                    ```
                * includes the following properties
                    * types
                        * used to define the structs that will be used in the message and specify their types
                        * is a mapping of string to a Type array
                            * example
                                ```
                                types := apitypes.Types{
                                	"EIP712Domain": {
                                		{Name: "name", Type: "string"},
                                		{Name: "version", Type: "string"},
                                		{Name: "chainId", Type: "uint256"},
                                		{Name: "verifyingContract", Type: "address"},
                                	},
                                	"ERC721Order": {
                                		{Name: "direction", Type: "uint8"},
                                		...
                                	},
                                	"Fee": { // custom types
                                    	{Name: "recipient", Type: "address"},
                                    	{Name: "amount", Type: "uint256"},
                                    	{Name: "feeData", Type: "bytes"},
                                    },
                                ```
                    * domain
                        * information specific to the protocol contract that the dapp used when asking for a signature
                        * designed to include bits of DApp unique information
                            * name
                                * human-readable string that represents the name of the domai
                                * often used to identify the dApp or smart contract associated with the message
                            * version
                                * string representing the version of the domain
                                * can be useful for distinguishing between different versions of the same dApp or smart contract
                            * chainId
                                * wallet providers should prevent signing if it does not match the network it is currently connected to
                                * it is crucial that chainId is verified on-chain
                                    * contracts have no way to find out which chain ID they are on
                                        * developers must hardcode chainId into their contracts and take extra care to make sure that it corresponds to the network they deploy on
                            * verifyingContract
                                * address of the smart contract that will verify the signed message
                        * domain separator
                            * information from the domain also needs to be hashed and used as a domain separator
                            * purpose is to disambiguate between two dapps with identical structures
                                * in order to avoid generating the same signatures for both
                                * example
                                    * two DApps come up with an identical structure like Transfer(address from,address to,uint256 amount)
                                    * with domain separator the dApp developers are guaranteed that there can be no signature collision
                    * primaryType
                        * string that represents the outermost type of the message object
                    * message
                        * contains the order element names as strings mapped to their values
                        * example
                            ```
                            {
                                amount: 100,
                                token: “0x….”,
                                id: 15,
                                bidder: {
                                    userId: 323,
                                    wallet: “0x….”
                                }
                            }
                            ```
                            can be split into two data structures
                            ```
                            Bid: {
                                amount: uint256,
                                bidder: Identity
                            }
                            Identity: {
                                userId: uint256,
                                wallet: address
                            }
                            ```
                    * example
                        ```
                        domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
                        typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.M
                        rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
                        hashBytes := keccak256(rawData)
                        hash := common.BytesToHash(hashBytes)
                        ```
            * standard for secure off-chain signature verification on the Ethereum blockchain
            * signature verification
                * process of checking that the address of the signer is equal to the address that you derive from the signature
                    ![Alt Text](img/verifying_signature.png)
                * ecrecover
                    * is vulnerable
                        * it interprets v values of both 27 and 28 as equivalent
                            * it only checks if v is greater than or equal to 27
                            * signatures produced with both v = 27 and v = 28 will yield the same public key
                        * it does not halt execution when invalid signatures are supplied
                            * it simply returns the address 0x0
                                * zero address in most contracts has a special meaning (i.e. burn address)
                                * smart contract might incorrectly assume that the zero address is a valid signer
                        * use: OpenZeppelin’s ECDSA library
                    * used to derive the address of a sender based on the digital signature
                    * needs assembly
                * example
                    * replicate this formatting/hash function
                    ```
                    struct Identity {
                        uint256 userId;
                        address wallet;
                    }
                    struct Bid {
                        uint256 amount;
                        Identity bidder;
                    }

                    string private constant IDENTITY_TYPE = "Identity(uint256 userId,address wallet)";
                    string private constant BID_TYPE = "Bid(uint256 amount,Identity bidder)Identity(uint256 userId,address wallet)";

                    uint256 constant chainId = 1;
                    address constant verifyingContract = 0x1C56346CD2A2Bf3202F771f50d3D14a367B48070;
                    bytes32 constant salt = 0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558;
                    string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)";
                    bytes32 private constant DOMAIN_SEPARATOR = keccak256(abi.encode(
                        EIP712_DOMAIN_TYPEHASH,
                        keccak256("My amazing dApp"),
                        keccak256("2"),
                        chainId,
                        verifyingContract,
                    ));

                    function hashIdentity(Identity identity) private pure returns (bytes32) {
                        return keccak256(abi.encode(
                            IDENTITY_TYPEHASH,
                            identity.userId,
                            identity.wallet
                        ));
                    }
                    function hashBid(Bid memory bid) private pure returns (bytes32){
                        return keccak256(abi.encodePacked(
                            "\\x19\\x01",
                           DOMAIN_SEPARATOR,
                           keccak256(abi.encode(
                                BID_TYPEHASH,
                                bid.amount,
                                hashIdentity(bid.bidder)
                            ))
                        ));

                    function verify(address signer, Bid memory bid, sigR, sigS, sigV) public pure returns (bool) {
                        return signer == ecrecover(hashBid(bid), sigV, sigR, sigS);
                    }
                    ```
            * wallets like Metamask can display the message in a more user-friendly manner
                * the signer can actually check the data before signing
                    * display in a human-readable way that users can understand and review before signing
                * example
                    ![Alt Text](img/eip721_metamask.png)
            * before EIP712
                * it was difficult for users to verify the data they were asked to sign
                * wallet signing interfaces would display a hashed message string
                    * the signer would have to assume that hash matched the message they thought they were signing
            * use case
                * meta-transactions
                    * relayer pays the gas fees on behalf of the user
                        * relayer = service responsible for submitting transactions on behalf of users
                    * users sign a request (off-chain) for a specific action they want to perform on the blockchain
                        * includes information like the target contract, function to call, and any required parameters
* short address attack
    * equivalent of minor SQL injection bug
    * problem: leading zeros is taken from the amount, and given to the shortened address
        * * means you've multiplied your amount by 1<<8 or 256
            * after the exchange has checked your balance on their internal ledger
        * reason: EVM pads all input data with 0s
        * example
            1. user B's address: 0xbbbbbb00
            1. user A's balance: 512 tokens
            1. A inputs B's address as 0xbbbbbb
            1. site (incorrectly) interprets this as a valid address and constructs transaction
                * selector: 0x01020304
                * transaction: 0x01020304bbbbbbbb00000001
            1. if we slice that back up into a 4-byte signature and 2 4-byte words
                * ['0x01020304', '0xbbbbbb00', '0x000001??']
            1. any index into the transaction data that hasn't been provided returns 0s
                * trailing zeros do not change the actual address
                    * `0x1234...5670` and `0x1234...567000` represent the same address
            1. final argument being interpreted by the EVM as 0x00000100 = 256
                * 256
            1. given that 256 is still less than 512 (your comment), the transaction succeeds
        * most vulnerable to this were large, shared wallets
            * example: exchange hot wallets
    * solution: smart contract must validate the length of an address input
* manipulating contract balance
    * problem: ether can be sent forcibly to a contract
        * if contract has some decision logic using `address(this).balance` - attacked can influence it
        * example
            ```
            selfdestruct(addressOfAttackedContract)
            ```
    * solution: there is no possible way to prevent forceful ether sending from happening
* Ethernaut game
    * Web3/Solidity based war game created by OpenZeppelin
    * each level is a smart contract that needs to be ‘hacked’
    * solutions: https://stermi.medium.com/lets-play-ethernaut-ctf-learning-solidity-security-while-playing-1678bd6db3c4

design patterns

* security
    * pull-over-push (withdrawal pattern)
        * example of the problem
            ```
            for(uint i = 0; i < users.length; i++) { users[i].transfer(amount); };”
            ```
            * if some address is a contract it may have continually failing fallback function
                * leads to whole transaction failure each time
        * solution: user should be able to claim their dividend from the contract
        * use cases
            * send ether/token to multiple addresses
            * avoid paying transaction fees (push transaction)
                * transaction initiator has to pay the transaction fee
                * users pay transaction fees (pull transaction)
    * access restriction
        * restricts unauthorized function calls
        * based on roles
        * use modifiers to check for the access rights
    * emergency stop
        * ability to pause the contract functions in unwanted situations
        * use cases
            * contract to be handled differently in case of any emergency situations
* creational patterns
    * factory
        * create a new child contract from a parent contract
        * https://eips.ethereum.org/EIPS/eip-1167
        * example
            * master contract can create a new child contract called Loan
                * Loan contract has logic to handle contract terms and conditions along with the funds as well
        * use case
            * new contract is required for each request to be processed
            * keep the funds separate in a different contract
* behavioral patterns
    * state machine
        * allows a contract to transition from different states
        * enables certain functions to be executed in each state
        * use cases
            * contract needs to have different set of functions based on the state
    * iterable map pattern
        * example
            ```
            mapping(uint256 => uint256) private data;
            uint256[] private keys;

            function removeValue(uint256 key) external {
                require(data[key] != 0, "Key does not exist");
                for (uint256 i = 0; i < keys.length; i++) {
                    if (keys[i] == key) {
                        // Swap the element to be removed with the last element
                        keys[i] = keys[keys.length - 1];
                        // Shorten the keys array by one
                        keys.pop();
                        break;
                    }
                }
                delete data[key];
            }
            ```
        * allows to iterate over the mapping entries
        * iteration over the mapping entries should not cause an out-of-gas exception
        * iteration should be used only in the view function
        * does not support removal of elements
        * use cases
            * need to filter some data out of the mapping
    * whitelisted addresses
        * maintain a curated list of addresses by the owner
        * use cases
            * whitelisted address allowed/disallowed to perform a certain task
* gas-optimization
    * worth to check: https://github.com/mtumilowicz/ethereum-gas-workshop
    * keccak256 for equality check
        * example: string equality
        * use case
            * gas-optimized solution for equality
    * variable packing
        * minimize slots used by storage
        * each storage slot is 32 bytes
        * use case
            * gas-optimized solution for storage
* life cycle
    * “Once a contract is destroyed, it cannot be recreated on the same address. ”
    * mortal pattern allows a contract to be destroyed from the Ethereum blockchain.”
        * “The mortal pattern should be used in the following cases, when:
            * You do not want a contract to be present on the blockchain once its job is finished
            * You want the ether held on the contract to be sent to the owner and the contract is not required further
            * You do not need the contract state data after the contract reaches a specific state”
    * auto deprecate
        * allows time-based access to certain function calls
        * example (using chainLink oracle)
            ```
            modifier onlyPremium() {
                require(subscriptionExpiry[msg.sender] >= getCurrentTime(), "Must be a premium member");
                _;
            }

            function getCurrentTime() internal returns (uint256) {
                Chainlink.Request memory req = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
                req.add("get", "https://chain.link/v1/time");
                req.add("path", "now");

                return sendChainlinkRequestTo(oracle, req, fee);
            }
            ```
        * use cases
            * allow/restrict a function call for a specified duration of time
            * auto-expired contract
            * periodic paid service-based model
            * existing user can purchase a premium status for a limited duration

ipfs

  • stands for Interplanetary File System
    1. suppose humanity has colonises Mars and the first person on Mars tries to access internet services from Earth
    2. it would approximately take 1 hour for him to access a news website
    3. what if another person tries?
      • he ends up taking another 1 hour and so on
      • but IFPS is used, the second person on Mars shall be able to retrieve the content from the first person
        • from there onwards data can spread like a wildfire
  • three main principles
    1. unique identification via content addressing
      • means that the content is going to determine the address
        • once something is added it can’t be changed
      • intuitive way to think about content for humans
        • example: ask someone for their favorite cat video
          • location answer: "the one on this server, at this sub-domain, under this file path, slash hilarious dash cat dot mp4"
          • description answer: "the one where the cat knocks the glass off the counter"
        • is generally not how we access content on the web today
          • http
            • refers objects (text files, pics, videos) by which server they are stored on
              • called "location based addressing"
                • location is the IP address or the domain name
                • if location isn’t accessible (the server is down), you won’t get resources
                  • there is a high probability that someone else out there has downloaded resource and still has a copy of it
                    • yet your computer won’t be able to grab it from that other person
              • IPFS moves from "location based addressing" to "content based addressing"
                • instead of creating an identifier that addresses things by location
                  • address it by some representation of the content itself
                • if in your browser you want to access a particular page then IPFS will ask the entire network "does anyone have this file that corresponds to this hash?"
                  • instead of saying where to find a resource, you just say what it is you want
            • is great for loading websites but it wasn’t designed for the transfer of large amounts data
              • example: audio, video files
              • files are downloaded from one server at a time
                • IPFS retrieves pieces of files from multiple nodes at once
                  • enables bandwidth savings of up to 60% for things like videos
              • enabled the emergence and mainstream success of alternative filesharing systems
                • example: Napster (music) and BitTorrent (movies and pretty much anything)
      • mechanism
        • overview Alt Text
        1. take a file
        2. hash it cryptographically
          • very small and secure representation of the file
          • hashes are actually something called a multihash
            • specifies which hash function it used
            • specifies the length of the resultant hash in the first two bytes of the multihash
            • example: hashes all seem to start with Qm
              • 12 denotes that this is the SHA256
              • output length is 20 in hex (or 32 bytes)
              • we get the Qm from when we base58 encode the whole thing
      • address usually starts with a hash that identifies some root object and then a path walking down
        • instead of a server, you are talking to a specific object and then you are looking at a path within that object
    2. content is linked via Merkle Directed Acyclic Graphs (DAGs)
      • hashes are used to reference data blocks and objects in a DAG
      • similar to Merkle tree
        • difference: Merkle DAG non-leaf nodes are allowed to contain data
      • need not be balanced
      • example: https://explore.ipld.io/#/explore/QmWNj1pTSjbauDHpdyg5HQ26vYcNWnubg1JehmwAE9NnU9
      • de-duplication by design
        • we reference content (not the files themselves)
        • adding a new file to the network will take less storage as the network gets larger
          • assuming the file is relatively similar to others in the network
    3. content discovery system is facilitated via Distributed Hash Tables (DHTs)
      • information about which node stores what blocks is organized as a distributed hash table
        • split across the nodes just like data itself
      • small values (equal to or less than 1KB) are stored directly on the DHT
      • for larger values, the DHT stores references (NodeIds of peers who can serve the block)
      • ask the network = query the distributed hash table
      • based on Kademlia
        • pretty common is p2p systems
      • example: find peers that can provide a particular bit of content
        ipfs dht findprovs QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ
        
  • distributed file system that seeks to connect all computing devices with the same system of files
    • small file (< 256 kB)
      • represented by an IPFS object
        • data = the file contents (plus a small header and footer)
        • no links, i.e. the links array is empty
    • large file (> 256 kB)
      • represented by an IPFS object
        • data = specifying that this object represents a large file
        • links = list of links to file chunks that are < 256 kB
    • directory is represented by a list of links to IPFS objects representing files or other directories
      • example: assume that all three files with an asterisk (*) contain the same text: “Hello World!/n”
        test_dir/
        ├── bigfile.js
        ├── *hello.txt // "Hello World!/n"
        └── my_dir
            ├── *my_file.txt // "Hello World!/n"
            └── *testing.txt // "Hello World!/n"
        
        DAG Alt Text
    • IPFS object
      • example
        $ ipfs object get QmarHSr9aSNaPSR6G9KFPbuLV9aEqJfTk1y9B8pdwqK4Rq // normally referred to by their Base58 encoded hash
        {
          "Links": [
            {
              "Name": "AnotherName",
              "Hash": "QmVtYjNij3KeyGmcgg7yVXWskLaBtov3UYL9pgcGK3MCWu",
              "Size": 18
            },
            {
              "Name": "SomeName",
              "Hash": "QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDC",
              "Size": 58
            }
          ],
          "Data": "Hello World!"
        }
        
      • Data - a blob of unstructured binary data of size < 256 kB
      • Links - an array of Link structures (links to other IPFS objects)
        • Name — the name of the Link
        • Hash — the hash of the linked IPFS object
        • Size — the cumulative size of the linked IPFS object, including following its links
      • note that the file name is not part of the IPFS object
        • two files with different names and the same content => same IPFS object representation
          • and hence the same hash
  • is essentially a P2P system for retrieving and sharing IPFS objects
  • acts as a decentralized source of data
    • distributed: processing is shared across multiple nodes
      • decisions may still be centralized and use complete system knowledge
    • decentralized: no single point where the decision is made
      • every node makes a decision for it’s own behaviour
      • resulting system behaviour is the aggregate response
  • self-certifying file system
    • peer node can perform authentication and encryption
      • when you initialize a new peer IPFS generates a pair of keys for it (private and public)
        $ ipfs init
        initializing IPFS node at ~/.ipfs/
        generating 2048-bit RSA keypair...done
        peer identity: Qm...
        
    • peers are identifying each other via their peer ID
      • is essentially a cryptographic hash of it’s public key
      • enables peers to find each other and authenticate themselves once they get connected
    • when two peers connect to each other they exchange public keys
      • connections between peers are encrypted and authenticated by default
      • authenticity of this data can be verified using the sender’s public key
  • operations
    • retrieving
    • adding
      • each node chooses which file to store
        • long-term IPFS storage: Filecoin
    • pinning
      • act of saving data on a node
      • prevents important data from being deleted from your node when the clearing process happens
      • you can only control and pin data on your node(s)
        • cannot force other nodes on the IPFS network to pin your content for you
        • to guarantee your content stays pinned, you have to run your own IPFS nodes
      • by pinning the file, other nodes on the network know they can access the file
  • is similar to a single BitTorrent swarm exchanging git objects
    • IPFS = one big swarm of peers for all data
      • difference: in BitTorrent each file has a separate swarm of peers (forming a P2P network with each other)
    • BitTorrent
      • is a successful and widely implemented protocol used to share large data files in a distributed manner
        • example: digital video sharing (TV shows, movies, video clips, etc)
      • splits large data files into segments
        • distributed over different nodes of a peer-to-peer network
        • result: data is being shared from more than one source
          • we are not exhausting a single server
  • BitSwap
    • their own exchange protocol
    • data trading module for IPFS
    • two primary jobs
      1. attempt to acquire blocks from the network that have been requested by the client peer (your local peer)
      2. judiciously (though strategically) send blocks of data that it already has in its possession to other peers who want those blocks
    • example
      1. verify that wantlist is empty (we aren’t in the middle of requesting anything)
        ipfs bitswap wantlist
        
      2. get a large file from the network
        ipfs get QmdpAidwAsBGptFB3b6A9Pyi5coEbgjHrL3K2Qrsutmj9K // Big Buck Bunny video
        
      3. query wantlist again
        ipfs bitswap wantlist // multiple hashes that are being requested from the network
        
  • consensus algorithm: Raft
    • peers coordinate their state
      • example: the list of CIDs which are pinned, their peer allocations and replication factor
    • is used to commit log entries to a "distributed log" which every peer follows
      • example: every "Pin" and "Unpin" requests are log entries in that log
        • when a peer receives a log "Pin" operation, it updates its local copy of the shared state
          • indicates that the CID is now pinned
    • Leader
      • election can only succeed if at least half of the nodes are online
      • only peer allowed to commit entries to the log
      • required for other parts of ipfs-cluster functionality (initialization, monitoring)
  • blockchain
    • problem: on the Ethereum platform you pay a rather large fee for storing data in the associated state database
      • minimizes bloat of the state database ("blockchain bloat")
      • solution: store not the data itself but hash of the data
        • distinction between storing a hash on the blockchain and storing the data on the blockchain becomes somewhat blurred
        • example: store an IPFS link in the blockchain
          • we can seamlessly follow this link to access the data as if the data was stored in the blockchain itself

tokens

  • any asset that is digitally transferable between two people
  • smart contract representing digital assets
    • simple "databases" on the blockchain
      • example
        • ERC20: stores balances of Ethereum addresses
        • ERC721: unique ID -> Ethereum address assignments
  • types
    • fungible asset
      • means that the individual units of an asset are interchangeable and essentially indistinguishable from one another
      • not unique and divisable entity
      • example: currency
        • $50 is always $50
    • non-fungible asset
      • means that the individual units of an asset are distinct and unique
        • often possessing specific attributes, characteristics, or properties that set them apart from one another
        • cannot be exchanged on a one-to-one basis
      • unique and indivisible entity
      • examples
        1. education and certification
          • Einstein diploma is not the same as Oppenheimer diploma
        2. diamonds
          • aren’t interchangeable as they all have different cuts, colours and sizes
          • can’t swap one diamond for another because they won’t be guaranteed to hold the same value
  • history
    • Bitcoin established the paradigm for other crypto projects: in order to issue any digital currency, a separate blockchain must be launched
      • rule has been broken by Ethereum: smart contacts enabled to create a token and assign it unique useful functions within your own application
    • ability for any developer to release their digital asset without the need for a separate blockchain has become a turning point in the history of cryptocurrencies
    • coin
      • native digital asset or cryptocurrency of a blockchain
      • example:
        • bitcoin blockchain => bitcoin (symbol: BTC)
        • ethereum blockchain => ether (symbol: ETH)
    • token
      • digital asset or cryptocurrency that is built on top of an existing blockchain is called a token
      • example (ERC20-compliant tokens built on the Ethereum blockchain)
        • Maker (symbol: MKR)
        • Augur (symbol: REP)
  • problem: supporting an increasing number of tokens became increasingly difficult
    • in order for the exchange or wallet to support the token, the creators had to write new code each time
    • example: prior to ERC-20,there was a problem with token compatibility because each of them had a unique smart contract
    • solution: standard protocol for all tokens (ERC)
      • play a pivotal role in ensuring interoperability and compatibility among different smart contracts and decentralized applications (DApps)
      • ERCs are analogous to RFCs
      • Ethereum Improvement Proposals (EIPs): focused on the Ethereum protocol
  • decentralization
    • nearly half of the top 20 projects can have their token transfers completely frozen by an owner (a single key or a multisig contract)
      • pausing can be valuable for future upgrades, swaps, and disaster mitigation but also leads to new risks

erc20

  • example: Tether (USDT)
    • value is pegged to the US dollar at a 1:1 ratio
    • commonly used to move funds between exchanges quickly and easily
  • use cases
    • reputation points of any online platform
    • lottery tickets and schemes
    • financial assets such as shares, dividends, and stocks of a company
    • fiat currencies, including USD
    • gold ounce
  • funglible
  • six functions, two events, and three information
    • functions
      1. totalSupply - returns the total supply of tokens that have been created for a particular project
        • problem: Solidity and the Ethereum Virtual Machine do not support decimals: only integer numbers can be used
          • solution: use larger integer values (the EVM supports 256-bit integers)
            • example: total supply 1000 tokens, with 18 decimal places (like Ethereum) = 1000*10**18
              _mint(msg.sender, 1000 * 10**18); // mining 1000 tokens
              transfer(recipient, 2 * 10**18); // sending 2 tokens
              
      2. balanceOf - returns the balance of tokens held by a particular address
      3. transfer - allows an address to send tokens to another address
        • the only transaction that happens on the blockchain is the contract call
        • transferring tokens from one account to another = simply update internal variable “_balances”
        • problem: Solidity and the Ethereum Virtual Machine do not support decimals: only integer numbers can be used
          • solution: token contract can use larger integer values (the EVM supports 256-bit integers)
            • example: 1000000000000000000 represents 1 ETH (with 18 decimal places)
              • transfer of 4000000000000000 will correspond to 0.004ETH being sent
        • token transfers are received at the contract silently
          • smart contract does not explicitly notify or acknowledge the receipt of tokens
          • solutions
            1. ERC223 standard provides tokenFallback method that will be called once the tokens are transferred
              function transfer(address to, uint value, bytes data) {
                      uint codeLength;
                      assembly {
                          codeLength := extcodesize(_to) // user wallets do not have code associated with them
                      }
                      balances[msg.sender] = balances[msg.sender].sub(_value);
                      balances[_to] = balances[_to].add(_value);
                      if(codeLength>0) {
                          // Require proper transaction handling.
                          ERC223Receiver receiver = ERC223Receiver(_to);
                          receiver.tokenFallback(msg.sender, _value, _data);
                      }
                  }
              
            2. approve plus transferFrom mechanism
              • example
                contract DEX {
                    Erc20 public token;
                
                    constructor(address _tokenAddress) {
                        token = Erc20(_tokenAddress);
                    }
                
                    function tradeTokens(address _buyer, uint256 _amount) external {
                        // Assume _amount is the number of tokens the buyer wants to purchase
                        require(token.transferFrom(_buyer, address(this), _amount), "Token transfer failed");
                        // DEX now holds _amount of tokens
                
                        // Perform trading logic here
                        // ...
                    }
                }
                
      4. approve - allows an address to approve another address to spend tokens on their behalf
        • used by decentralized exchanges
          • there are two types of exchanges
            • centralized exchange
              • example: Binance
              • you have to send your cryptocurrency coin or token to the exchange's account
                • then they allow you to trade on their platform
              • trust issue: coins and tokens are held on an exchange's account
                • if the exchange's account is hacked, you will lose your cryptocurrencies
            • decentralized exchange
              • example: Uniswap
              • all coins/token are kept in your wallet only
              • directly trade via exchange platform
        • front-running approval attack
          • problem: you change your approve amount to a given contract
            • example: reduce the amount approved from 1 ETH to 0.5 ETH
              • approved contract can race to transfer the money you initially approved (the 1 ETH)
                • and then also spend the money you just approved (0.5 ETH)
          • solutions
            1. use the increaseApproval() or decreaseApproval() functions
            2. ensures that, before updating the value, it should be set to zero
              require(_value == 0 || allowed[msg.sender][_spender] == 0);
              
      5. transferFrom - allows an address to transfer tokens from another address that has approved them to do so
        • prerequisite: approver must have called the approve() function prior
        • if called from within a Solidity contract recommended to enclose with require
          require(ERC20.transferFrom(from, to, value)) // in case of any transfer failure, the transaction should revert
          
      6. allowance - returns the amount of tokens that an approved address can spend on behalf of another address
      • developers can also add additional functions and features
        • example: OpenZeppelin implementation of ERC20 contracts
          • _mint(), _burn(), _burnFrom(), etc
    • information
      1. name - returns the name of the token
      2. symbol - returns the symbol of the token
        • usually a few letters or characters that represent the token
      3. decimals - returns the number of decimal places that the token can be divided into
        • example: a token with 18 decimal places can be divided into 10^18 units
    • event
      • transfer(address indexed _from, address indexed _to, uint256 _value)
        • triggered whenever tokens are transferred
        • minting emits transfer event with the 0 address as the source
      • approval(address indexed _owner, address indexed _spender, uint256 _value)
        • triggered on any call to approve() function

erc721

  • example: domain name
  • use cases
    • unique digital content piece
      • through royalty implementations in the smart contract, original creators can receive a predetermined percentage of sales whenever the NFT changes hands
    • real estate property
    • social media content Tweets, Videos, and pictures
    • gaming assets and collectibles
    • gaming characters
  • non-fungible
    • each NFT has a numerical identifier (uint256) called TokenID
  • is a digital receipt of something you purchased
    • digital certificates of authenticity
    • example: when you buy an NFT, you are buying a piece of data that points to a server that hosts that image
      • so, what you own, is not the access to the server, and not the image itself, but rather that tiny piece of data that points to the server
  • represents a class of assets, whereas an ERC20 token represents a particular type of asset
    • entire collection comes from a single ERC-721 contract, with each item having its own TokenID
  • smart contracts define the ownership and transfer rights of a particular digital asset
  • case studies
    • art
      • artists don’t have to rely on gallery shows or live auctions to be able to sell their art
        • monetize their projects without the physical resources needed to do that in the real world
    • collectibles
      • sports leagues including the NFL, MLB and NBA have all created digital collections
    • gaming and virtual reality
      • players will invest more when digital assets can be
        • transferred between games or platforms
        • traded on open markets, they will invest more of their hard-earned cash
    • licenses and certifications
      • easily verifiable with university smart contract
  • functions
    • erc20 like
      • balanceOf(address _owner)
      • transferFrom(address _from, address _to, uint256 _tokenId)
      • approve(address _approved, uint256 _tokenId)
      • setApprovalForAll(address _operator, bool _approved)
      • getApproved(uint256 _tokenId)
      • isApprovedForAll(address _owner, address _operator)
    • function ownerOf(uint256 _tokenId)
    • function safeTransferFrom(address _from, address _to, uint256 _tokenId)
      • ensure the destination address can handle the NFT, preventing accidental loss
      • is used to check if the address receiving the token is an ERC-721 receiver or not
        • ERC721TokenReceiver
          • minimal acceptable feature for onERC721Received is to do nothing and this is how they have implemented it
        • goal is to ensure that your NFT does not get locked up in an address from which you can never retrieve it
    • function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes _data)
      • allows for additional data to be passed to the receiving contract
      • example
        { // _data
          "rarity": "Rare",
          "strength": 80,
          "abilities": ["Flying", "Fire Breath"]
        }
        
        then
        contract MyCardRegistry {
            struct CardAttributes {
                string rarity;
                uint256 strength;
                string[] abilities;
            }
        
            mapping(uint256 => CardAttributes) public cardAttributes;
        
            function parseDataToCardAttributes(bytes calldata _data) internal pure returns (CardAttributes memory) {
                // Parse JSON data
                (string memory rarity, uint256 strength, string[] memory abilities) = abi.decode(_data, (string, uint256, string[]));
        
                return CardAttributes({
                    rarity: rarity,
                    strength: strength,
                    abilities: abilities
                });
            }
        
            function onCardReceived(address _sender, uint256 _cardId, bytes calldata _data) external returns (bool) {
                // Parse the _data and update cardAttributes with the received attributes
                CardAttributes memory attributes = parseDataToCardAttributes(_data);
        
                // Add the card to the registry
                cardAttributes[_cardId] = attributes;
        
                return true;
            }
        }
        
      • developers can also add additional functions and features
        • example: OpenZeppelin implementation of ERC721 contracts
          • tokenApprovals, operatorApprovals, etc
  • information
    • erc20 like
      • name()
      • symbol()
    • tokenURI(tokenId)
      • points to the metadata
      • allowing platforms and wallets to display the NFT’s distinct attributes
      • problem: centralized nft
        • returns the metadata location in the form of: HTTP://centralizedserver.com/TokenID
          • here is nothing stopping the image host from changing the image which the URL points to
            • example: https://example.com/nft/1
              {
                "name": "One Ring to Rule Them All",
                "description": "The One Ring, forged in the fires of Mount Doom, grants immense power to its bearer.",
                "image": "https://example.com/one_ring.jpg",
                "attributes": [
                  {
                    "trait_type": "Type",
                    "value": "Artifact"
                  },
                  {
                    "trait_type": "Rarity",
                    "value": "Legendary"
                  },
                  {
                    "trait_type": "Power",
                    "value": "Dominion over all other rings"
                  },
                  {
                    "trait_type": "Owner",
                    "value": "Sauron"
                  }
                ],
                "external_url": "https://example.com/one_ring",
                "franchise": "The Lord of the Rings",
                "lore": "Forged by the Dark Lord Sauron to control the other Rings of Power, the One Ring is a malevolent artifact of great evil."
              }
              
        • solution: IPFS
          • problem: cannot set ipfs://contendidentifierhash/TokenID
            • IPFS can only provide an immutable hierarchical file system structure
            • requirement: all tokens and TokenIDs are set at the time of the smart contract deployment
            • solution: TokenID as a pointer itself to the metadata
              • TokenID=IPFScontentidentifierhash
              • idea by Titusz Pan
              • we would then just add "ipfs://" in front
              • example
                $ ipfs add MetaDataIPFSToken.json cid-version=1 hash=blake2b-208
                added bafkzvzacdkm3bu3t266ivacqjowxqi3hvpqsyijxhsb23rv7nj7a MetaDataIPFSToken.json
                
                then convert it to hex “-b=base16”
                $ ipfs cid format -b=base16 bafkzvzacdkm3bu3t266ivacqjowxqi3hvpqsyijxhsb23rv7nj7a
                f01559ae4021a99b0d373d7bc8a80504bad782367abe12c21373c83adc6bf6a7e
                
                without f0 gives a token ID
                constructor() public ERC1155("ipfs://f0{id}") { // "f0" is not part of the CID, but rather a representation of the format being used (base16)
                
  • events
    • erc20 like
      • event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
      • event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    • event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
  • OpenSea
    • refers to itself as the "amazon of NFTs"
    • has a large and wide variety of different types of NFTs

erc1155

  • before
    • problem: game with 100,000 items = 100,000 smart contracts
      • ERC-1155 developer Witek Radomski pointed out: that's like needing a different phone for each app you use
      • solution: ERC1155
        • allows for multiple token collections to be launched in just one smart contract
  • called fungible-agnostic standard
    • allows multiple token to be represented by a single contract, regardless of fungibility
  • allows the creation of fungible, non-fungible, and semi-fungible tokens all in one contract
    • semi-fungible tokens
      • possesses characteristics of both fungible and non-fungible tokens
        • certain units of the token may be interchangeable with other units
        • others may have distinct characteristics, making them distinguishable from one another
      • example: trading cards
        • fungible: can still be traded within the same collection
        • non-fungible: not completely interchangeable with each other
  • makes use of TokenID
    • each token ID to represent a new configurable token type, which may have its own metadata, supply and other attributes
      • supply is only 1 <=> NFT
  • supports secure atomic swaps
    • all tokens are inside one contract
  • use case
    • blockchain-based decentralized games, as games need coins and collectibles
    • creating and managing digital art and collectible tokens with different levels of rarity, editions, and properties
  • can save costs by managing tokens in batches (approval, transfer and balance) instead of individually
  • functions
    • safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes calldata _data)
      • token transfers to other contracts may revert with the following message: ERC1155: transfer to non ERC1155Receiver implementer
        • implementation: ERC165 - interface detection
        • means that the recipient contract has not registered itself as aware of the ERC1155 protocol => transfers to it are disabled
        • example: Golem contract
          • currently holds over 350k GNT tokens (multiple tens of thousands of dollars) and lacks methods to get them out of there
            • has happened to virtually every ERC20-backed project, usually due to user error
      • transfer call must revert if: _to address is 0
    • safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _amounts, bytes calldata _data)
      • any number of items can be sent in a single transaction to one or more recipients
      • reducing transaction costs and complexity
    • balanceOf(address _owner, uint256 _id)
      • differs from ERC20: has an additional id argument for the identifier of the token that you want to query the balance of
    • balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids)
    • setApprovalForAll(address _operator, bool _approved)
      • intentionally designed with simplicity in mind: can only approve everything for one address
    • isApprovedForAll(address _owner, address _operator)
  • events
    • TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value)
    • TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values)
    • ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved)
  • must implement all of the functions in the ERC1155TokenReceiver interface to accept transfers
    • onERC1155Received
    • onERC1155BatchReceived

ERC165

  • before
    • determining a smart contract’s supported interfaces was a challenging and gas-consuming process
    • some interaction might involve sending test transactions to the contract and observing the behavior
      • if the contract didn't support the desired interface, the transaction would fail or return an unexpected result
  • problem: interfaces
    • are not explicitly represented on the blockchain
    • used by developers during the coding and development process
    • applications must usually simply trust they are not making an incorrect call
      • contract declaring its interface can be very helpful in preventing errors
    • solution: ERC165
  • known as the Standard Interface Detection
  • we can publish and detect all interfaces a smart contract implements
    • accounts simply declare their interfaces
      • they are not required to actually implement them
      • must not be relied on for security
    • example
      • publish
        contract MyContract is ERC165 {
            function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
                return interfaceID == type(ERC165).interfaceId; // defined as the XOR of all function (it implements) selectors
            }
        }
        
      • verify
        function checkIfContractSupportsERC165(address contractAddress) external view returns (bool) {
            ERC165 erc165 = ERC165(contractAddress);
            return erc165.supportsInterface(INTERFACE_ID_ERC165);
        }
        
  • use case
    • play a vital role in enabling seamless interactions between smart contracts and DApps
      • example: OpenSea - NFT Marketplace
        • uses ERC165 to check if a given contract supports the ERC721 standard, which is the most common standard for non-fungible tokens
        • if a contract doesn't support ERC721 (as indicated by the supportsInterface call), OpenSea will handle it differently

openzeppelin

  • an open-source library of protocols, templates, & utilities for smart contract development
  • includes implementations for token standards, flexible role-based permissioning schemes, & reusable components
    • in particular: offers implementations for ERC20 , ERC721, & ERC1155

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.