Smart Contract Development
  • Introduction
    • What is a Transaction
    • Accounts and Signing
    • What is a smart contract
  • Learning Solidity
    • Introduction
    • Module 1
      • Variable Types
      • Variable Scope: State & Local variables
      • Global variables
      • Functions
        • View and Pure
        • Shadowing in Fuctions
      • Mapping
      • Require
      • Events
    • Project #1: Simple Registry
    • Module 2
      • Constructor
      • Data Location: Value & Reference
      • Interface
      • Import
        • Importing in Foundry
      • Inheritance
      • ERC-20
      • Checks-effect-interaction pattern
    • Project #2: Basic Vault
    • Module 3
      • Payable
      • Receive
      • Fallback
      • Returns
    • Project #3: ERC20+ETH Wrapper
    • Module 4
      • Immutable and Constant
      • Fixed-point Math
      • Abstract contracts
      • ERC-4626
      • Modifier + Inheritance +Ownable
      • Type
    • Project #4: Fractional Wrapper
    • Module 5
      • If-else
      • Libraries
        • TransferHelper
      • Chainlink Oracle
    • Project #5: Collateralized Vault
  • Compendium
    • Solidity Basics
      • Variable Types
      • Value Types
        • address
        • enum
      • Reference Types
        • strings
        • mappings
        • struct
        • Arrays
        • Multi-Dimensional arrays
      • Global Objects
      • Functions
        • Function types
        • Constructor Function
        • Transaction vs Call
        • Require, Revert, Assert
      • Function signature + selectors
      • Payable
        • Payable + withdraw
        • msg.value & payable functions
      • Receive
      • Fallback function (sol v 0.8)
        • Fallback function (sol v 0.6)
      • call, staticcall, delegatecall
    • Return & Events
    • Control Variable Visibility
    • Local Variables (Storage v Memory)
    • Data Location and Assignment Behaviors
    • Modifiers & Inheritance & Import
      • import styles
    • Interface & Abstract Contracts
    • ABI & Debugging
    • Libraries
    • Conditional(ternary) operators
    • Smart Contract Life-cycle
      • Pausing Smart Contracts
      • Destroying Smart Contracts
    • Merkle Trie and MPT
    • Merkle Tree Airdrop
  • Try & catch
  • Ethereum Signatures
  • EVM, Storage, Opcodes
    • EVM
    • Wei, Ether, Gas
    • Storage
    • ByteCode and Opcodes
    • Transaction costs & Execution costs
  • Reading txn input data
  • Data Representation
  • Yul
    • Yul
      • Intro
      • Basic operations
      • Storage Slots
      • Storage of Arrays and Mappings
      • Memory Operations
      • Memory: how solidity uses memory
      • Memory: Return, Require, Tuples and Keccak256
      • Memory: Logs and Events
      • Inter-contract calls
      • calldata
      • free memory pointer
    • Yul Exercises
      • read state variable
      • read mapping
      • iterate Array, Return Sum
    • memory-safe
  • Upgradable Contracts
    • Upgradability & Proxies
    • UUPS Example
    • Minimal Proxy Example
    • TPP Example
    • 🚧Diamond
      • On Storage
  • Gas Opt
    • Block Limit
    • gasLimit & min cost
    • Solidity Optimiser
    • Memory v calldata
    • Memory caching vs direct storage vs pointers
    • < vs <=
    • reverting early
    • X && Y, ||
    • constant and immutable
    • caching sload into mload
    • Syntactic Sugar
    • using unchecked w/o require
    • Compact Strings
    • Calling a view function
    • Custom errors over require
    • usage of this.
      • multiple address(this)
  • ERCs & EIPs
    • ERC-20.sol
      • Core functions
      • transfer()
      • transferFrom()
      • TLDR transfer vs transferFrom
    • Landing
      • ERC721.sol
      • EIP-721
        • LooksRare
        • Page 1
      • ERC-1271
      • EIP-2981
      • ERC-165
      • EIP-1167: Minimal Proxy Contract
    • VRFConsumerBase
    • UniswapV2Library
  • Yield Mentorship 2022
    • Projects
      • #1 Simple Registry
      • #2 Basic Vault
      • #3 ERC20+ETH Wrapper
        • setFailTransferTrue
      • #4 Fractional Wrapper
      • #5 Collateralized Vault
        • Process
        • Vault.sol
        • Testing
        • Chainlink Oracles
        • Pricing + Decimal scaling
        • Refactor for Simplicity
      • #9 Flash Loan Vault
        • Implementing ERC3156
        • Full code for lender
        • Ex-rate calculation
    • State Inheritance Testing
    • Testing w/ Mocks
    • Yield Style Guide
    • Github Actions
    • TransferHelper.sol
    • math logic + internal fn
    • Interfaces: IERC20
  • Foundry
    • Overview
    • Importing Contracts
    • Testing
      • stdError.arithmeticError
      • assume vs bound
      • Traces
      • label & console2
      • std-storage
  • Smart Contract Security
    • Damn Vulnerable Defi
      • 1. Unstoppable
      • 2. Naive receiver
      • 3. Truster
      • 4. Side Entrance
      • 5. The Rewarder
      • 6. Selfie
      • 7. Compromised
      • 8. Puppet
      • 9. Puppet V2
      • 10 - Free Rider
    • Merkle Tree: shortened proof attack
  • Fixed-Point Math
    • AMM Math
  • Solidity Patterns
    • checks-effects-interactions pattern
    • Router // batch
    • claimDelegate: stack unique owners
    • claimDelegate: cache previous user
  • Array: dup/ascending check
  • Deployment
    • Behind the Scenes
    • Interacting with External Contracts
    • Logging, Events, Solidity, Bloom Filter
  • Misc
    • Mnemonic Phrases
    • Bidul Ideas
  • Archive
    • Brownie Framework
      • Brownie basics
        • storing wallets in .env
        • Deployment to ganache
        • Interacting with contract
        • Unit Testing
        • Testnet deployment
        • Interacting w/ deployed contract
        • Brownie console
      • Brownie Advanced
        • Dependencies: import contracts
        • helpful_scripts.py
        • verify and publish
        • Forking and Mocking
        • Mocking
        • Forking
      • Testing
      • Scripts Framework
        • deploy.py
        • get_accounts
        • deploy_mocks()
        • fund_with_<token>()
      • Brownie Networks
    • Brownie Projects
      • SharedWallet
        • Multiple Beneficiaries
        • Common Code Contract
        • Adding Events
        • Renounce Ownership
        • Separate Files
      • Supply Chain
        • ItemManager()
        • Adding Events
        • Adding unique address to each item
      • Lottery
      • Aave - Lending and Borrowing
        • Approve & Deposit
        • Borrow
      • NFT
      • Advanced Collectible
        • adv_deploy() + Testing
        • Create Metadata
        • Setting the TokenURI
    • node npm
    • Ganache
    • Truffle
    • Remix
    • Installing Env
Powered by GitBook
On this page
  • Objectives
  • Blueprint
  • Base Code
  • Events
  • Problem:
  • Solution: Using Events
  • Testing
  • Linear vs State Inheritance Approach
  • Deployment
  • Verifying
  • Check verification
  1. Learning Solidity

Project #1: Simple Registry

Objectives

  • Users (identified by an address) can claim a name, which is recorded on-chain.

  • Once a name has been claimed, no other user can claim it.

  • A name owner can release a name.

  • A user can claim any number of names.

Blueprint

  • A mapping to serve as a registry.

  • Function to register names.

  • Function to release ownership of a name.

  • Events to announce state changes for each function.

Base Code

SimpleNameRegister.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

/**
@title An on-chain name registry
@author Calnix
@notice A registed name can be released, availing to be registered by another user
*/
contract SimpleNameRegister {
    
    /// @notice Map a name to an address to identify current holder 
    mapping (string => address) public holder;    

    /// @notice Emit event when a name is registered
    event Register(address indexed holder, string name);

    /// @notice Emit event when a name is released
    event Release(address indexed holder, string name);

    /// @notice User can register an available name
    /// @param name The string to register
    function register(string calldata name) external {
        require(holder[name] == address(0), "Already registered!");
        holder[name] = msg.sender;
        emit Register(msg.sender, name);
    }

    /// @notice Holder can release a name, making it available
    /// @param name The string to release
    function release(string calldata name) external {
        require(holder[name] == msg.sender, "Not your name!");
        delete holder[name];
        emit Release(msg.sender, name);
    }
}

1. Mapping: associating names to their respective owner's address

mapping (string => address) public holder;    
  • initialized as a public state variable.

  • solidity will automatically create a getter function for it, which we can use to pass a name as parameter to check ownership.

  • If there is no owner, the address returned will be 0x0000000000000000000000000000000000000000

2. Function to allow a user to register their ownership of a name

    function register(string calldata name) external {
        require(holder[name] == address(0), "Already registered!");
        holder[name] = msg.sender;
        emit Register(msg.sender, name);
    }
  • require statement checks if the name passed is available to be claimed.

  • If available, passed name is mapped to the address of the msg.sender via the mapping holder

  • event Registered emitted each time a name is registered.

3. Function to allow a user to relinquish their ownership of a name

    function release(string calldata name) external {
        require(holder[name] == msg.sender, "Not your name!");
        delete holder[name];
        emit Release(msg.sender, name);
    }
  • require statement checks if the name passed is indeed registered to the function caller (msg.sender)

  • If so, we reset the mapped address of the name to the zero address.

    • mappings can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to default values.

    • the default value for address type will be 0x0000000000000000000000000000000000000000 or address(0)

  • event Releaseemitted each time a name is relinquished.

Events

Events allow us to “print” information on the blockchain in a way that is more searchable and gas efficient than just saving to public storage variables in our smart contracts.

Problem:

  • A single address could be the owner of multiple names.

  • It would be useful if the end-user could simply supply their address to check which are the names registered to them.

  • Achieving this one to many structure on-chain would not be gas efficient.

Solution: Using Events

  • Run a background task that subscribes to events from the contract.

  • This background task will listen to events as they are emitted and track the list of addresses for each owner via some centralized database.

  • Website front-end can read from this database and reflect accordingly.

Testing

Testing in Solidity is somewhat different from other frameworks like brownie, as they would be done in Solidity. Essentially we deploy a test contract containing our test functions.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "ds-test/test.sol";
import 'src/SimpleNameRegister.sol';

interface CheatCodes {
    function prank(address) external;
    function expectRevert(bytes calldata) external;
    function startPrank(address) external;
    function stopPrank() external;
}

contract SimpleNameRegisterTest is DSTest {
    
    // declare state var.
    SimpleNameRegister simpleNameRegister;
    CheatCodes cheats;
    address adversary;

    function setUp() public {
        simpleNameRegister = new SimpleNameRegister();
        cheats = CheatCodes(HEVM_ADDRESS);
        adversary = 0xE6A2e85916802210147e366D4431f5ca4dD51a78;
    }

    // user can register an available name
    function testRegisterName(string memory _testString) public {
        simpleNameRegister.registerName(_testString);
        bool success = (address(this) == simpleNameRegister.nameOwner(_testString));
        assertTrue(success);
    }
    
    // user can register an available name and relinquish it
    function testRelinquishName(string memory _testString) public {
        simpleNameRegister.registerName(_testString);   
        simpleNameRegister.relinquishName(_testString);
        bool success = (simpleNameRegister.nameOwner(_testString) == address(0));
        assertTrue(success);
    }

    // user cannot relinquish a name that does not belong to them
    function testRelinquishAsNotOwner(string memory _testString) public {
        simpleNameRegister.registerName(_testString);   
        cheats.startPrank(adversary);
        cheats.expectRevert(bytes("The provided name does not belong to you!"));
        simpleNameRegister.relinquishName(_testString);        
        cheats.stopPrank();
    }
    
    // user cannot register a name that already has an owner
    function testRegisterUnavailableName(string memory _testString) public {
        simpleNameRegister.registerName(_testString);   

        cheats.startPrank(adversary);
        cheats.expectRevert(bytes("The provided name has already been registered!"));
        simpleNameRegister.registerName(_testString);   
        cheats.stopPrank();
    }
}
pragma solidity ^0.8.13;

import "ds-test/test.sol";
import 'src/SimpleNameRegister.sol';

import "forge-std/console2.sol";

interface CheatCodes {
    function prank(address) external;
    function expectRevert(bytes calldata) external;
    function label(address addr, string calldata label) external;
}

abstract contract StateZero is DSTest {
    SimpleNameRegister public simpleNameRegister;
    CheatCodes cheats;
    address user;
    
    function setUp() public virtual {
        simpleNameRegister = new SimpleNameRegister();
        cheats = CheatCodes(HEVM_ADDRESS);
        user = 0x0000000000000000000000000000000000000001;
        cheats.label(user, "user");
    }
}

contract StateZeroTest is StateZero {

    function testCannotRelease(string memory testString) public {  
        console2.log("Cannot release a name that you do not hold");
        cheats.prank(user);
        cheats.expectRevert(bytes("Not your name!"));
        simpleNameRegister.release(testString);
    }

    function testRegister(string memory testString) public {
        console2.log("User registers a name");
        cheats.prank(user);
        simpleNameRegister.register(testString);
        bool success = (user == simpleNameRegister.holder(testString));
        assertTrue(success);
    }
}

abstract contract StateRegistered is StateZero {
    address adversary;
    string name;

    function setUp() public override {
        super.setUp();
        adversary = 0xE6A2e85916802210147e366D4431f5ca4dD51a78;
        cheats.label(adversary, "adversary");
        
        // state transition
        name = 'whale';
        cheats.prank(user);
        simpleNameRegister.register(name);
    }
}

contract StateRegisteredTest is StateRegistered {

    function testAdversaryCannotRegisterName() public {
        console2.log("Adversary cannot register name belonging to User");
        cheats.prank(adversary);
        cheats.expectRevert(bytes("Already registered!"));
        simpleNameRegister.register(name);   
    }

    function testAdversaryCannotReleaseName() public {
        console2.log("Adversary cannot release name belonging to User");
        cheats.prank(adversary);
        cheats.expectRevert(bytes("Not your name!"));
        simpleNameRegister.release(name);   
    }

    function testUserCannotRegisterOwnedName() public {
        console2.log("User cannot register a name that he already holds");
        cheats.prank(user);
        cheats.expectRevert(bytes("Already registered!"));
        simpleNameRegister.register(name);
    }

    function testUserRelease() public {
        console2.log("User releases name that he holds");
        cheats.prank(user);
        simpleNameRegister.release(name);
        bool success = (address(0) == simpleNameRegister.holder(name));
        assertTrue(success);
    }
}

function setUp()

The setup function is invoked before each test case is run. Serves to setup the necessary variables and conditions for your test functions to operate.

  • deploy a fresh instance of SimpleNameRegister before each test function is ran via simpleNameRegister = new SimpleNameRegister()

  • address adversary will be used in test 3 & 4

Linear vs State Inheritance Approach

Deployment

forge create src/SimpleNameRegister.sol:SimpleNameRegister --private-key ${PRIVATE_KEY_EDGE} --rpc-url ${ETH_RPC_URL}
  • Forge can deploy only one contract at a time.

  • Solidity files may contain multiple contracts. :MyContract above specifies which contract to deploy from the src/MyContract.sol file

Verifying

forge verify-contract --chain-id ${KOVAN_CHAINID} --compiler-version v0.8.13+commit.abaa5c0e ${CONTRACT_ADDRESS} src/SimpleNameRegister.sol:SimpleNameRegister ${ETHERSCAN_API_KEY} --num-of-optimizations 200

Set --num-of-optimizations 200

If not set on deployment, foundry defaults to 200. So make sure you pass --num-of-optimizations 200 if you left the default compilation settings

Check verification

forge verify-check --chain-id ${KOVAN_CHAINID} ${GUID} ${ETHERSCAN_API_KEY}

Running the above three commands manually, each time can be bothersome. To that end, create a new file 'MakeFile' in the project root directory if one does not exist.

# include .env file and export its env vars (-include to ignore error if it does not exist)
include .env

deploy:
	forge create src/SimpleNameRegister.sol:SimpleNameRegister --private-key ${PRIVATE_KEY_EDGE} --rpc-url ${ETH_RPC_URL}

verify:
	forge verify-contract --chain-id ${KOVAN_CHAINID} --compiler-version v0.8.13+commit.abaa5c0e ${CONTRACT_ADDRESS} src/SimpleNameRegister.sol:SimpleNameRegister ${ETHERSCAN_API_KEY} --num-of-optimizations 200 --flatten

verify-check:
	forge verify-check --chain-id ${KOVAN_CHAINID} ${GUID} ${ETHERSCAN_API_KEY}

Now you will only need to run:

  • make deploy

  • make verify

  • make verify-check

Be sure to update the contract address and GUID values in .env for each deployment as they would change.

Note: For make verify, the --flatten flag may ONLY be necessary on a window machine. If verification fails, try without it. If it still fails, its in god's hands now.

export PRIVATE_KEY_EDGE = 0x1232141356361...42414
export ETH_RPC_URL = https://kovan.infura.io/v3/b2a555d8b6e64178bbb736aabaf93a1a
export ETHERSCAN_API_KEY = I5CK8CJ7FZ6BWQ6YGFD4U3E7CFTNWQJ3A3
export Contract = src/SimpleNameRegister.sol:SimpleNameRegister
export KOVAN_CHAINID = 42

export CONTRACT_ADDRESS = 0xa155f6b896d1a22b2c18cb6229e4e0760176199b
export GUID = dyrsthjrhefssx6epi91ky8dxdun69ufbmz5rgqt6dzxzcgt4w
PreviousEventsNextModule 2

Last updated 2 years ago

For more on events:

Each test is run as independent cases -> changes made in a prior test function will not spill out of scope into a following test function (See more: )

For more about CheatCodes:

Installing make on windows:

https://calnix.gitbook.io/solidity-lr/return-and-events#events
https://calnix.gitbook.io/solidity-lr/foundry/testing#to-show-that-there-is-no-spillover
https://calnix.gitbook.io/solidity-lr/yield-mentorship-2022/simple-registry-1/state-inheritance-testing
https://stackoverflow.com/questions/32127524/how-to-install-and-use-make-in-windows
https://app.gitbook.com/s/Tgomzlmn9NrxUY0OQ3cD/foundry/testing#cheatcodes