🐸
maxAPY for Devs
  • maxAPY ERC7540
    • MetaVault
      • Introduction
      • Architecture
      • Core Operations
      • State Management & Operations
      • Asset Management
      • Settlement Mechanics
      • Withdrawal Queue Mechanics
      • Deployed Addresses
  • Periphery
    • Hurdle Rate Oracle
    • SharePriceOracle
    • Deployed Addresses
  • maxAPY ERC4626
    • Introduction
    • Architecture
    • Vault
      • MaxApyFactory
      • MaxApyRouter
      • MaxApyVault
    • Periphery
      • MaxApyHarvester
    • Base Strategies
      • BaseStrategy
      • BaseSommelierStrategy
      • BaseYearnV3Strategy
      • BaseYearnV2Strategy
      • BaseConvexStrategy
      • BaseConvexStrategyPolygon
      • BaseBeefyStrategy
      • BaseHopStrategy
      • BaseBeefyCurveStrategy
    • Strategies
      • Ethereum - WETH
        • Convex
          • ConvexdETHFrxETHStrategy
        • Sommelier
          • SommelierMorphoEthMaximizerStrategy
          • SommelierStEthDepositTurboStEthStrategy
          • SommelierTurboDivEthStrategy
          • SommelierTurboEEthV2Strategy
          • SommelierTurboEthXStrategy
          • SommelierTurboEzEthStrategy
          • SommelierTurboRsEthStrategy
          • SommelierTurboStEthStrategy
          • SommelierTurboSwEthStrategy
        • Yearn
          • YearnAaveV3WETHLenderStrategy
          • YearnAjnaWETHStakingStrategy
          • YearnCompoundV3WETHLenderStrategy
          • YearnV3WETH2Strategy
          • YearnV3WETHStrategy
          • YearnWETHStrategy
      • Ethereum - USDC
        • Convex
          • ConvexCrvUSDWethCollateralStrategy
        • Sommelier
          • SommelierTurboGHOStrategy
        • Yearn
          • YearnAjnaDAIStakingStrategy
          • YearnDAIStrategy
          • YearnLUSDStrategy
          • YearnUSDCStrategy
          • YearnUSDTStrategy
      • Polygon - WETH
        • Hop
          • HopETHStrategy
      • Polygon - USDC.e
        • Convex
          • ConvexUSDCCrvUSDStrategy
          • ConvexUSDTCrvUSDStrategy
        • Beefy
          • BeefyCrvUSDUSDCeStrategy
          • BeefyMaiUSDCeStrategy
          • BeefyUSDCeDAIStrategy
        • Yearn
          • YearnAaveV3USDTLenderStrategy
          • YearnAjnaUSDCStrategy
          • YearnCompoundUSDCeLenderStrategy
          • YearnDAILenderStrategy
          • YearnDAIStrategy
          • YearnMaticUSDCStakingStrategy
          • YearnUSDCeLenderStrategy
          • YearnUSDCeStrategy
          • YearnUSDTStrategy
    • Subgraph
      • Overview
      • Schema
      • Query Guide
Powered by GitBook
On this page
  • Overview
  • Key Features
  • Architecture
  • Contract Inheritance
  • Core Concepts
  • Setup and Configuration
  • Administrative Functions
  • User Functions
  • Security Considerations
  • JavaScript Source Requirements
  • Events
  • Error Handling
  • Integration Examples
  • Gas Efficiency
  • Limitations
  • Appendix
  1. Periphery

Hurdle Rate Oracle

PreviousPeripheryNextSharePriceOracle

Last updated 2 months ago

Overview

The Hurdle Rate Oracle is a Solidity smart contract designed to fetch and store interest rates for multiple tokens in a gas-efficient manner. It leverages Chainlink Functions to retrieve external data and implements a bitmap-based storage mechanism that allows scaling up to 16 different token rates without increasing operational costs.

The primary focus of this contract is to track rates for liquid staking tokens like Lido and USDY, though it can be extended to support other tokens as well.

Github:

Key Features

  • Fetches and stores rates for multiple tokens using Chainlink Functions

  • Uses a bitmap-based storage system for efficient data packing

  • Supports up to 16 different token rates in a single storage slot

  • Implements security features including reentrancy protection and rate update throttling

  • Includes admin controls for managing tokens and configuration

Architecture

The contract is built on several core components:

  1. Chainlink Functions Integration: Fetches external data through decentralized oracle networks

  2. Bitmap Storage: Packs up to 16 different token rates into a single storage slot

  3. Access Control: Implements ownership controls for administrative functions

  4. Security Measures: Includes reentrancy protection and rate manipulation prevention

Contract Inheritance

HurdleRateOracle
├── FunctionsClient (Chainlink)
├── ConfirmedOwner (Chainlink)
└── ReentrancyGuard (Solady)

Core Concepts

Rate Storage Mechanism

The contract uses an efficient bitmap-based storage system:

  • Each token rate occupies 16 bits (2 bytes) in the currentRates storage slot

  • Rates are stored in basis points (10000 bps = 100%)

  • Up to 16 different token rates can be stored in a single storage slot

  • Each token is assigned a specific position (0-15) in the bitmap

Rate Update Process

  1. An external caller triggers the updateRates() function

  2. The contract makes a Chainlink Functions request using the configured JavaScript source

  3. The Chainlink DON (Decentralized Oracle Network) executes the JavaScript code

  4. The callback function receives and stores the new rates

Rate Retrieval

Rates can be accessed in several ways:

  • getRate(address token): Returns the rate for a specific token

  • getAllRates(): Returns all rates as a packed bitmap

  • getRateByPosition(uint8 position): Returns the rate at a specific position

Setup and Configuration

Prerequisites

  • Chainlink Functions subscription

  • LINK tokens to fund the subscription

  • JavaScript source code for rate fetching

Initialization

The contract is initialized with:

  • Chainlink Functions router address (Base Mainnet: 0xf9B8fc078197181C841c296C876945aaa425B278)

  • Subscription ID (default: 36)

  • JavaScript source code for rate fetching

By default, the contract registers:

  • WETH (Base) at position 0 - This will be used for Lido

  • USDCe (Base) at position 1 - This will be used for USDY

Deployment Script

The contract can be deployed using the provided Foundry script:

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

import {Script} from "forge-std/Script.sol";
import {HurdleRateOracle} from "../src/HurdleRateOracle.sol";

contract DeployHurdleRateOracle is Script {
   // Chainlink Functions Router address for Base Mainnet
   address constant ROUTER = 0xf9B8fc078197181C841c296C876945aaa425B278;
   
   string constant DEFAULT_SOURCE = 
       "const apiResponse = await Functions.makeHttpRequest({"
       "url: `https://api.maxapy.io/hurdle-rate`,"
       "headers: {'Content-Type': 'application/json'}"
       "});"
       "if (apiResponse.error) throw Error('API Request Error');"
       "const { data } = apiResponse;"
       "return Functions.encodeUint256(data.bitmap);";

   function run() external returns (HurdleRateOracle) {
       uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
       uint64 subscriptionId = 36;

       vm.startBroadcast(deployerPrivateKey);
       
       HurdleRateOracle oracle = new HurdleRateOracle(
           ROUTER,
           subscriptionId,
           DEFAULT_SOURCE
       );

       vm.stopBroadcast();
       
       return oracle;
   }
}

Default API Integration

By default, the contract is configured to fetch rates from the Maxapy API endpoint (https://api.maxapy.io/hurdle-rate), which returns a pre-formatted bitmap of rates for the registered tokens.

Administrative Functions

Token Management

function registerToken(address token, uint8 position) external onlyOwner

Registers a new token at a specific position in the bitmap.

Configuration

function setSource(string memory _source) external onlyOwner

Updates the JavaScript source code for Chainlink Functions requests.

function setSubscriptionId(uint64 newSubscriptionId) external onlyOwner

Updates the Chainlink Functions subscription ID.

function setPause(bool _paused) external onlyOwner

Pauses or unpauses the contract.

User Functions

Updating Rates

function updateRates() external nonReentrant isNotPaused

Triggers a rate update via Chainlink Functions. This function can be called by anyone, but is protected by a minimum time interval between updates.

Reading Rates

function getRate(address token) external view returns (uint16 rate, uint256 timestamp)

Returns the current rate for a specific token and the timestamp of the last update.

function getAllRates() external view returns (uint256)

Returns all current rates as a packed bitmap.

function getRateByPosition(uint8 position) external view returns (uint16)

Returns the rate at a specific position in the bitmap.

Security Considerations

The contract implements several security features:

  1. Reentrancy Protection: Uses the ReentrancyGuard to prevent reentrancy attacks

  2. Rate Update Throttling: Enforces a minimum time interval between updates to prevent manipulation

  3. Owner Controls: Administrative functions are restricted to the contract owner

  4. Circuit Breaker: Includes a pause mechanism to stop operations in emergency situations

JavaScript Source Requirements

The JavaScript source code for Chainlink Functions must:

  1. Return a packed bitmap of rates

  2. Format each rate as a uint16 in basis points (0-10000)

  3. Position each rate correctly in the bitmap

  4. Handle errors gracefully

Default Source Code

The contract comes with a default source code that fetches rates from the Maxapy API:

const apiResponse = await Functions.makeHttpRequest({
  url: `https://api.maxapy.io/hurdle-rate`,
  headers: {'Content-Type': 'application/json'}
});
if (apiResponse.error) throw Error('API Request Error');
const { data } = apiResponse;
return Functions.encodeUint256(data.bitmap);

This source code:

  1. Makes an HTTP request to the Maxapy API

  2. Checks for errors in the response

  3. Extracts the pre-formatted bitmap

  4. Encodes it as a uint256 for the contract

Custom API Integration

To create a custom API integration, you'll need to ensure your API returns data in a format that can be converted to a bitmap. The expected format is:

{
  "bitmap": "0x0000000000000000000000000000000000000000000000000000000001F401F4"
}

Where bitmap is a hexadecimal representation of a uint256 with packed rates. For example, the above bitmap represents:

  • Position 0: 500 bps (0x01F4)

  • Position 1: 500 bps (0x01F4)

  • All other positions: 0 bps

Events

The contract emits the following events:

  • RateUpdateRequested(bytes32 indexed requestId): Emitted when a rate update is requested

  • RateUpdateFulfilled(bytes32 indexed requestId, uint256 newRates, uint256 timestamp): Emitted when rates are updated

  • TokenRegistered(address indexed token, uint8 position): Emitted when a new token is registered

  • RequestFailed(bytes32 indexed requestId, bytes error): Emitted when a Chainlink Functions request fails

  • SubscriptionIdUpdated(uint64 oldSubId, uint64 newSubId): Emitted when the subscription ID is updated

  • SourceUpdated(string newSource): Emitted when the source code is updated

  • PauseStateChanged(bool paused): Emitted when the pause state is changed

Error Handling

The contract uses custom errors to provide clear failure messages:

  • InvalidPosition: Position is out of range (must be 0-15)

  • RequestNotFound: Request ID doesn't match any pending request

  • TokenNotRegistered: Attempted to get rate for unregistered token

  • TokenAlreadyRegistered: Attempted to register token at occupied position

  • UpdateTooFrequent: Rate update requested too soon after previous update

  • InvalidRates: Returned rates are invalid

  • InvalidSubscription: Subscription ID is invalid

  • RateLimit: Too many rates provided

  • InvalidSubscriptionId: Invalid subscription ID provided

  • Paused: Contract is paused

  • AddressZero: Zero address provided

  • InvalidSource: Empty source code provided

Integration Examples

Reading a Token's Rate

// Get the current rate for WETH (Lido)
(uint16 rate, uint256 timestamp) = hurdleRateOracle.getRate(0x4200000000000000000000000000000000000006);

// Convert basis points to percentage
uint256 ratePercentage = rate / 100; // e.g. 500 bps = 5.00%

Checking Multiple Rates

// Get all rates at once
uint256 allRates = hurdleRateOracle.getAllRates();

// Extract individual rates
uint16 lidoRate = uint16(allRates & 0xFFFF);
uint16 usdyRate = uint16((allRates >> 16) & 0xFFFF);

Updating Rates from a Smart Contract

// Trigger a rate update
try hurdleRateOracle.updateRates() {
    // Update requested successfully
} catch Error(string memory reason) {
    // Handle error
}

Updating Rates via JavaScript

You can trigger rate updates using a simple Node.js script:

import { ethers } from "ethers";
import dotenv from 'dotenv';
dotenv.config();

async function requestUpdate() { 
  const oracleAddress = "0x5EDd8bD98d96404a2387C2fD37b48d363DF67803";
  
  const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  const oracleAbi = [
    "function updateRates() external"
  ];

  const oracle = new ethers.Contract(oracleAddress, oracleAbi, wallet);

  try {
    console.log("\nRequesting rate update...");
    const tx = await oracle.updateRates({
      gasLimit: 500000
    });
    
    console.log("Transaction sent! Hash:", tx.hash);
    console.log("\nWaiting for confirmation...");
    
    const receipt = await tx.wait();
    console.log("Transaction confirmed!");
    console.log("Gas used:", receipt.gasUsed.toString());
    
  } catch (error) {
    console.error("Error:", error);
  }
}

requestUpdate().catch(console.error);

This script requires an .env file with:

  • RPC_URL: Base network RPC URL

  • PRIVATE_KEY: Private key of the wallet that will pay for the transaction

Gas Efficiency

The use of bitmap storage provides significant gas savings:

  • Reading rates costs minimal gas due to efficient packing

  • Updating all rates costs the same as updating a single rate

  • No iteration required to access specific rates

Limitations

  1. Maximum of 16 token rates per contract instance

  2. Rate precision limited to basis points (0.01%)

  3. Rate updates are limited by the MIN_UPDATE_INTERVAL (1 hour)

  4. Dependent on Chainlink Functions subscription being funded with LINK

Appendix

Constants

  • MAX_RATE_BPS: Maximum allowed rate in basis points (10000 = 100%)

  • CALLBACK_GAS_LIMIT: Gas limit for Chainlink Functions callback (300,000)

  • MIN_UPDATE_INTERVAL: Minimum time between rate updates (1 hour)

Understanding Basis Points

  • 1 basis point = 0.01%

  • 100 basis points = 1%

  • 10000 basis points = 100%

https://github.com/VerisLabs/hurdleRateOracle