0xedB0...3484

Turn your stories into NFTs

0 Following     0 Follower

Optimised Oracle contract

After a few articles reviewing traditional oracle contracts (1, 2, 3), we can finally have a look at a revised, optimised and much simpler version of an price oracle smart contract.

I won't dive into the details, it's quite heavily commented.

One of my main goal was to write a contract that YOU could understand, and then tweak to your needs if necessary.

Most of the contracts you find on the web are pre Solidity 0.8 contracts, that still use SafeMath. This one doesn't as it is written for Solidity 0.8.

The main focus of this contract was to KISS (Keep It Small and Simple).

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

/**
  From https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol

  Fixed window oracle that computes the Time Weighted Average Price for the entire epoch once every epoch.

  The TWAP is only guaranteed to be over at least 1 epoch, but may be over more
  in case the update() function has not been called for a long time.

  Strongly simplified so that:
    - we don't use the obscure FixedPoint library
        we simply >> 112 and << 112 when necessary
    - we don't rely on UniswapV2OracleLibrary.sol
        it does too much for our need
    - we don't use UniswapV2Library
        it does too much for our need
    - we don't care about the price of FTM in CHRY (aka token1 / token0)
    - we don't care about time overflow: get real boys!
        uint32 time will overflow in 2106!

    With these simplifications, we get a much simpler oracle that is:
      - much easier to understand
      - only cares about one token price
      - self contained, no dependency
      - much more gas efficient
      - compiles with solidity 0.8

  Owner powers:
    - none

  Some Maths:

    reserves are in uint112
    prices   are in uint224  (aka uq112x112)
      the 112 left  bits are the integer part
      the 112 right bits are the decimal part
    cumulative prices are in uint256 = uint224 price * uint32 time interval

    To compute a price:   price = (uint224(reserveToken0) << 112) / uint224(reserveToken1)
    "<< 112" is equivalent to "* 2**112"
*/



// From OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)
// simplified (not is Context)

abstract contract Ownable {

  // ==== Events      ====
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  // ==== Storage     ====
  // Private so it cannot be changed by inherited contracts
  address private _owner;

  // ==== Constructor ====
  constructor() {
    _transferOwnership(msg.sender);
  }

  // ==== Modifiers   ====
  modifier onlyOwner() {
    require(_owner == msg.sender, "Ownable: caller is not the owner");
    _;
  }

  // ==== Views       ====
  function owner() public view virtual returns (address) {
    return _owner;
  }

  // ==== Mutators    ====
  function renounceOwnership() public virtual onlyOwner {
    _transferOwnership(address(0));
  }

  function transferOwnership(address newOwner_) public virtual onlyOwner {
    require(newOwner_ != address(0), "Ownable: new owner is the zero address");
    _transferOwnership(newOwner_);
  }


  // ==== Internals   ====
  function _transferOwnership(address newOwner_) internal virtual {
    address oldOwner = owner();
    _owner = newOwner_;
    emit OwnershipTransferred(oldOwner, newOwner_);
  }
}

// Minimal interface to Liquidity Pool contract.
// Here I'm only interested in the token1 (CHRY) and don't care about token0 (FTM)
interface IPool {

    // returns contract address of the ERC20 token1
    function token1() external view returns (address);

    // returns the reserves of the liquidity pool
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);

    // returns the last cumulative price of token 1
    function price1CumulativeLast() external view returns (uint256);
}



contract CherryOracle is Ownable {

  // ==== Events ====
  event Updated(uint32 indexed epoch, uint112 twap);


  // ==== Constants ====
  uint256 public constant PERIOD = 6 hours;

  address public constant pool   = 0xF2aDD870885c1B3E50B7cBA81a87C4d0291cBf6F;
  address public constant token  = 0x4C41bFf37db389BA1edC7ED7e757059a1D08ceb5;


  // ==== Storage ====
  // ---- Slot 1  ----
  uint32  public epoch;
  uint32  public blockTimestampLast;
  uint112 public twap;
  // ---- Slot 2  ----
  uint256 public price1CumulativeLast;


  // ==== Constructor ====
  constructor() {

    // coherence check:
    // we call the pool.token1() to make sure its the right one
    require(IPool(pool).token1() == token, "Hey, you took the wrong Spooky LP");

    // set blockTimestampLast
    uint112 reserveFTM;
    uint112 reserveCHRY;
    (reserveFTM, reserveCHRY, blockTimestampLast) = IPool(pool).getReserves();

    // make sure liquidity pool not empty
    require(reserveFTM  > 0, 'Oracle: FTM  reserve empty');
    require(reserveCHRY > 0, 'Oracle: CHRY reserve empty');

    // set the initial token cumulative price from LP
    price1CumulativeLast =  IPool(pool).price1CumulativeLast();

    // set initial Epoch
    epoch = uint32(getEpoch());

    // The TWAP cannot be initialised.
    // It will be zero until update() has been successfully called.
  }


  // Current epoch
  function getEpoch() public view returns (uint256) {
    return block.timestamp / PERIOD;
  }


  // Update the state of the Oracle once every epoch.
  function update() external returns(bool) {
    // only once per epoch
    uint256 newEpoch = getEpoch();

    // don't do this or you can lock summertime
    // require(newEpoch > epoch, "Oracle: wait until next epoch");

    // do this instead
    if (newEpoch <= epoch) {
      return false;
    }

    // below code will only be run once per epoch

    // update epoch
    epoch = uint32(newEpoch);

    // get last cumulative prices from liquidity pool
    uint256 price1CumulativeNew = IPool(pool).price1CumulativeLast();

    // get last time the liquidity pool was updated
    (uint256 reserve0, uint256 reserve1, uint256 lastPoolUpdate) = IPool(pool).getReserves();

    // if time has elapsed since the last update of the liquidity pool
    if (lastPoolUpdate < block.timestamp) {
      // we mock the accumulated price increase
      unchecked {
        price1CumulativeNew += ((reserve0 << 112) / reserve1) * ( block.timestamp - lastPoolUpdate );
      }
    }

    // compute new twap = acc price delta / time delta
    uint112 newTwap;
    unchecked {
      newTwap = uint112( ((price1CumulativeNew - price1CumulativeLast) / ( block.timestamp - blockTimestampLast)) * 1e18 >> 112);
    }

    // write down new state to storage
    blockTimestampLast   = uint32(block.timestamp);
    price1CumulativeLast = price1CumulativeNew;
    twap                 = newTwap;

    emit Updated(uint32(newEpoch), newTwap);

    return true;
  }
}


Gas costs

Magik Oracle (classic Tomb fork oracle)

If we compare it to Magik's Oracle:

Deploy cost: 1 632 151 gas

Cost of calling the update() function: 68 512 gas

Remember that this function will most probably be called by other contracts, so keeping its gas cost as low as possible is important.

This Oracle

Then, my optimised Oracle (currently in use to compute cherries twap):

Deploy cost: 553 066 gas

Cost of calling the update() function: 44 317 gas

. . . . .

If you have a comment, I invite you to share it here. We all need to learn and improve, so maybe one day, we'll stop being junior smart contract devs. 😉

If you found this article interesting or useful, please consider liking it as well. Thanks.

Reactions are currently disabled. They will return soon.

⏪ Previous Story

0xedB0...3484 avatar

Oracle Smart Contract Review 3

This article follows Oracle Smart Contract Review 2 where we dived into FixedPoint and SafeMaths to do ...
by 0xedB0...3484

⏩ Next Story

0xedB0...3484 avatar

Cherry Merry and Tomb Forks

In this article, we'll see why Cherry Merry is an improved kind of Tomb fork, where Cherries act like Tomb ...
by 0xedB0...3484