solidity101 icon indicating copy to clipboard operation
solidity101 copied to clipboard

A basic introduction to solidity, at the ETHLisbon hackathon.

#+TITLE: Solidity 101

  • Link https://github.com/hrkrshnn/solidity101
  • Resources to get started
  • https://soliditylang.org/
  • https://docs.soliditylang.org/en/v0.8.9/
  • https://cryptozombies.io/
  • Basic Faucet

#+begin_src solidity :tangle Faucet.sol // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.9;

/// Comments that start with three slashes: NatSpec comments. /// /// A faucet that holds ETH distributes it later. contract Faucet { /// Function that allows the Faucet to receive ETH /// Anyone can send eth to the address of the contract. /// Notice the keyword payable. receive() payable external { }

  /// Sends back 0.1 ether to the sender
  function claim() external {
      payable(msg.sender).transfer(0.1 ether);
  }

}

#+end_src

  • Faucet that tracks claims

#+begin_src solidity :tangle TrackingFaucet.sol // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.9;

contract TrackingFaucet { mapping(address => uint) claims; /// Function that allows us to receive ETH receive() payable external { }

   /// Sends back 0.1 ether to the sender
   function claim() external {
       // Note: in real code try to use custom errors
       // instead of revert strings
       require(
           claims[msg.sender] <= 0.2 ether,
           "You claimed too much."
       );
       claims[msg.sender] += 0.1 ether;
       payable(msg.sender).transfer(0.1 ether);
   }

}

#+end_src

  • Proof of work faucet #+begin_src solidity :tangle POWFaucet.sol // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.9;

     contract POWFaucet {
         /// Mapping.
         /// A hash table with key being an address
         /// and a unsigned integer as value.
         /// Notice the keyword public.
         /// Public keyword for state variables automatically
         /// creates a getter function.
         /// Getter function with signature
         /// "function claims(address) view returns (uint);"
         mapping(address => uint) public claims;
    
         receive() payable external {
         }
    
         /// Sends back 0.1 ether to the sender, up to 3 times.
         /// Proof of work is required for claiming ether.
         /// Needs to call claim function with a unique integer for
         /// each address, which is obtained by work!
         ///
         /// TODO: the same proof of work can be used for three times.
         /// Implement it in such a way that you will need different
         /// proof of work numbers for each turn.
         /// Hint: nonces.
         function claim(uint pow) external {
             bytes memory data = abi.encodePacked(msg.sender, pow);
             uint256 hash = uint256(keccak256(data));
             // TODO: try to use custom errors instead of strings.
             require(hash % 10 == 0, "Proof of work required");
    
             // TODO: try to use custom errors instead of strings.
             require(claims[msg.sender] <= 0.2 ether, "You claimed too much");
    
             claims[msg.sender] += 0.1 ether;
    
             // TODO: <address payable>.transfer has some issues if
             // the address executes code on ETH transfer or for certain proxy
             // contracts. What is the problem? How do we fix it?
             // What about re-entrancy?
             // Hint: msg.sender.call{value: 0.1 ether)("");
             payable(msg.sender).transfer(0.1 ether);
         }
     }
    

#+end_src

  • Proof of work contract #+begin_src solidity :tangle ProofOfWork.sol // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.9;

    /// A contract for finding the proof of work number /// Note: this does not really have to be done using a contract, or on chain. contract ProofOfWork { /// Returns unsigned integer that can be used to claim eth from the faucet. function pow() external view returns (uint) { // Assuming that keccak256 is uniformly distributed, // it takes around 10 tries for "keccak256(random_number) % 10 == 0" // to be true. // We do this in a for loop. for (uint i = 0; i < 15; i++) { bytes memory data = abi.encodePacked(msg.sender, i); uint256 hash = uint256(keccak256(data)); if (hash % 10 == 0) return i; } revert("Could not find a hash"); } } #+end_src

#+RESULTS:

  • Design Tips
  • Have a specification for each function and for the entire contract:
    • Who should be able to call the function?
    • What external calls will the function make? Are they trusted or untrusted?
    • What state variables should the function modify?
    • When should the function revert?
    • Should the function be able to receive ETH?
    • Should the contract hold ETH or tokens?
  • Design Tips
  • Architecture that minimizes gas usage:
    • Minimize: storage writes (around 20000 gas) storage reads (around 2100 gas) and external calls (around 2600 gas).
  • Do not use Proxies or Upgradable contracts unless absolutely necessary.
  • Try to avoid centralization, e.g., one address with a lot of power in the smart contract.
  • Reuse existing libraries: https://github.com/OpenZeppelin/openzeppelin-contracts