4. Fund Me

Sending ETH through a function & reverts

contract FundMe {

    // Smart Contracts can hold funds like wallets can
    function fund() public payable {
        // Want to be able to set a mininum fund amount in USD
        // 1. How do we send ETH to this contract?

        // require means this function needs this specific condition to fulfill to run
        // else revert : undo any action before, and send remaining gas back
        // 1e18 === 1 * 10 ** 18 === 1000000000000000000 === value in wei of 1 ethereum
        require(msg.value > 1e18, "Didn't send enough!");
    }
}

  • Oracles are centralized network which introduce single point of failure

  • Chainlinks are Decentralized Oracle Network

  • Chainlink has many features out of the box

Chainlink Data Feeds are the quickest way to connect your smart contracts to the real-world market prices of assets. https://docs.chain.link/docs/data-feeds/price-feeds/

It is a provably fair and verifiable random number generator (RNG) that enables smart contracts to access random values without compromising security or usability. They are also used in building blockchain games and NFTs.

Enables conditional execution of your smart contracts based on trigger of an event. https://docs.chain.link/docs/chainlink-automation/

Chainlink nodes can request any API using any api. It requires LINK tokens for the transaction. https://docs.chain.link/docs/any-api/


Interfaces and Price Feeds

contract FundMe {
    function getVersion() public view returns (uint256) {
        // ABI
        // Address (Goerli ETH Testnet)	0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e

        // Method 1 of ABI : Copy the Interface code and paste it in this contract
        // Interface is the template code (.h file in c++) which includes
        // function definations and not the actual implementation

        AggregatorV3Interface priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
        return priceFeed.version();
    }
}

https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol

interface AggregatorV3Interface {
  function decimals() external view returns (uint8);

  function description() external view returns (string memory);

  function version() external view returns (uint256);

  function getRoundData(uint80 _roundId)
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

  function latestRoundData()
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );
}

Importing from GitHub & NPM

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract FundMe {
    function getVersion() public view returns (uint256) {
        // ABI
        // Address (Goerli ETH Testnet)	0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e

        // Method 2 (Proper Way) : Import directly from Github via NPM Package

        AggregatorV3Interface priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
        return priceFeed.version();
    }
}

Floating Point Math in Solidity

contract FundMe {
    uint256 public mininumUsd = 50 * 1e18 ; // 1 * 10 ** 18

    function fund() public payable {
        require(getConversionRate(msg.value) >= mininumUsd, "Didn't send enough!");
        // 18 decimals
    }

    function getPrice() public view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
        (,int256 price,,,) = priceFeed.latestRoundData();
        // ETH in terms of USD
        // 1500.00000000 (8 decimal places)
        return uint256(price * 1e10); // 1**10 == 1000000000 == 18 decimal places
    }

    function getConversionRate(uint256 ethAmount) public view returns (uint256) {
        uint256 ethPrice = getPrice();
        // Always multiply before devide
        uint256 ethAmountInUsd = (ethPrice * ethAmount); // 36 decimals
        ethAmountInUsd /= 1e18; // 18 decimals
        return ethAmountInUsd;
    }
}

Libraries

  • Similar to contracts but you can't declare any state variable and you can't send ether

  • It is embedded into contract if all library functions are internal

  • Otherwise, library must be deployed and then linked before the contract is deployed

PriceConverter.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

library PriceConverter {
    function getPrice() internal view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
        (,int256 price,,,) = priceFeed.latestRoundData();
        return uint256(price * 1e10);
    }

    function getConversionRate(uint256 ethAmount) internal view returns (uint256) {
        uint256 ethPrice = getPrice();
        uint256 ethAmountInUsd = (ethPrice * ethAmount);
        ethAmountInUsd /= 1e18;
        return ethAmountInUsd;
    }
}

FundMe.sol

contract FundMe {
    using PriceConverter for uint256;

    uint256 public mininumUsd = 50 * 1e18 ;

    address[] public funders;
    mapping(address => uint256) public addressToAmountFunded;

    function fund() public payable {
        // If function had 2 parameters like
        // getConversionRate(uint256 value, uint somethingElse) {}
        // We can call like this
        // msg.value.getConversionRate(12);
        require(msg.value.getConversionRate() >= mininumUsd, "Didn't send enough!");
        funders.push(msg.sender);
        addressToAmountFunded[msg.sender] = msg.value;
    }
}

SafeMath, Overflow Checking, and the "unchecked" keyword

  • Was used widely before version 0.8 of solidity

  • Unsigned integers were going unchecked which led to overflows

    • uint256 bigNumber = 255

    • bigNumber = bigNumber + 1

    • // value becomes 0

  • Safemath library was used for checking the upper limits to avoid overflows

  • Onward version 0.8, this check was automatically added

    • We can still use unchecked {} keyword to revert back to unchecked mode


For Loops

    function withdraw() public {
        for(uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
            address funder = funders[funderIndex];
            addressToAmountFunded[funder] = 0;
        }
    }

Resetting an Array

        // 0 means reset to blank array, 1 means reset to array of size 1
        funders = new address[](0);

Withdrawing Ethereum

        // Three Different Ways

        // 1. Transfer
        // capped at 2300 gas, if more gas used, throws error
        // Automatically returns if transaction failed

        // msg.sender = address
        // payable(msg.sender) = payable address

        // payable(msg.sender).transfer(address(this).balance);

        // 2. Send
        // capped at 2300 gas, returns boolean
        // Don't automatically returns if transaction failed

        // bool sendSuccess = payable(msg.sender).send(address(this).balance);
        // require(sendSuccess, "Send failed");

        // 3. Call
        // forwards all gas or set gas, returns boolean
        // it is a lower level command which returns 2 variables i.e bool & bytes memory dataReturned

        (bool callSuccess,) = payable(msg.sender).call{value: address(this).balance}("");
        require(callSuccess, "Call failed");

Constructor

contract FundMe {

    address public owner;

    constructor() {
        owner = msg.sender;
    }

}

Modifiers

    function withdraw() public onlyOwner {
        for(uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
            address funder = funders[funderIndex];
            addressToAmountFunded[funder] = 0;
        }

        funders = new address[](0);

        (bool callSuccess,) = payable(msg.sender).call{value: address(this).balance}("");
        require(callSuccess, "Call failed");
    }

    modifier onlyOwner {
        require(msg.sender == owner, "Sender is now owner!");
        _; // Doing rest of code
    }

Advanced Solidity Concepts

Immutable & Constant

  • Used to make contract gas efficient

    • Instead of storing these variables in the storage class, we save them in the byte code of the contract hence gas efficient.

  • If a variable is assigned value at compile time and is never changed, then we can add constant keyword with it.

    • Naming convention is usually all caps

  • Variables that we set one time but not on the same line where they are declared, we add immutable keyword with it.

    • Name convention is usually i_<name-of-var>

    // Initialized on same line
    uint256 public constant MINIMUM_USD = 50 * 1e18 ;

    // Initialized later on in contract
    address public immutable i_owner;

    constructor() {
        i_owner = msg.sender;
    }

Custom Errors

  • Introduced in version 0.8.4

  • We can declare custom errors on reverts

  • Used for gas efficiency

    • Since a string is called in require statement

error NotOwner();

contract FundMe {

    modifier onlyOwner {
        // require(msg.sender == i_owner, "Sender is now owner!");
        if(msg.sender != i_owner) { revert NotOwner(); }
        _;
    }

}

Receive & Fallback

  • What happens if someone send this contract ETH without calling any function

contract FallbackExample {

    uint256 public result;

    // Dont add function keyword with receive since it is a special function
    // Other special funtions are Constructor(), Fallback(), etc

    // Trigerred when someone send ETH to contract without calling any ftn and
    // also when there is no call data with the transaction
    receive() external payable {
        result = 1;
    }


    // Trigerred when someone send ETH to contract without calling any ftn and
    // also when there is call data with the transaction
    fallback() external payable {
        result = 2;
    }
}

Final Contracts Samples

FundMe.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./PriceConverter.sol";

error NotOwner();

contract FundMe {

    using PriceConverter for uint256;

    uint256 public constant MINIMUM_USD = 50 * 1e18 ;

    address[] public funders;
    mapping(address => uint256) public addressToAmountFunded;

    address public immutable i_owner;

    constructor() {
        i_owner = msg.sender;
    }

    function fund() public payable {
        require(msg.value.getConversionRate() >= MINIMUM_USD, "Didn't send enough!");
        funders.push(msg.sender);
        addressToAmountFunded[msg.sender] = msg.value;
    }

    function withdraw() public onlyOwner {
        for(uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
            address funder = funders[funderIndex];
            addressToAmountFunded[funder] = 0;
        }

        funders = new address[](0);

        (bool callSuccess,) = payable(msg.sender).call{value: address(this).balance}("");
        require(callSuccess, "Call failed");
    }

    modifier onlyOwner {
        // require(msg.sender == i_owner, "Sender is now owner!");
        if(msg.sender != i_owner) { revert NotOwner(); }
        _;
    }

    receive() external payable {
        fund();
    }

    fallback() external payable {
        fund();
    }
}

PriceConverter.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

library PriceConverter {


    function getPrice() internal view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
        (,int256 price,,,) = priceFeed.latestRoundData();
        return uint256(price * 1e10);
    }

    function getConversionRate(uint256 ethAmount) internal view returns (uint256) {
        uint256 ethPrice = getPrice();
        uint256 ethAmountInUsd = (ethPrice * ethAmount);
        ethAmountInUsd /= 1e18;
        return ethAmountInUsd;
    }

}

Last updated