EtfOracle

Description:

Decentralized Finance (DeFi) protocol contract providing Liquidity, Factory, Oracle functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "hopium/etf/main/etf-oracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/common/interface/imDirectory.sol";
import "hopium/etf/types/etf.sol";
import "hopium/etf/interface/imEtfFactory.sol";
import "hopium/uniswap/interface/imUniswapOracle.sol";
import "hopium/common/types/bips.sol";
import "hopium/common/lib/full-math.sol";
import "hopium/common/interface-ext/iErc20Metadata.sol";
import "hopium/etf/interface/iEtfOracle.sol";

abstract contract Storage {
    uint256 internal constant WAD = 1e18;
}

abstract contract Utils is ImUniswapOracle, Storage {
    /// @dev 10**exp with safety guard. 10**78 overflows uint256.
    function _pow10(uint8 exp) internal pure returns (uint256) {
        require(exp <= 77, "pow10 overflow");
        // unchecked is fine due to the guard
        unchecked { return 10 ** uint256(exp); }
    }

    /// @dev Scale arbitrary decimals to 1e18 without overflow using mulDiv.
    function _scaleTo1e18(uint256 amount, uint8 decimals) internal pure returns (uint256) {
        if (decimals == 18) return amount;
        if (decimals < 18) {
            // amount * 1e18 / 10^dec
            return FullMath.mulDiv(amount, WAD, _pow10(decimals));
        } else {
            // amount * 1e18 / 10^dec  == amount / 10^(dec-18)
            return amount / _pow10(decimals - 18);
        }
    }

    /// @dev Convert a WETH-denominated 1e18 amount to USD 1e18 using the oracle.
    ///      Returns 0 if either the amount or the oracle price is 0.
    function _convertWethToUsd(uint256 wethAmount18) internal view returns (uint256 usdAmount18) {
        if (wethAmount18 == 0) return 0;
        uint256 wethUsdPrice18 = getUniswapOracle().getWethUsdPrice(); // USD per 1 WETH, 1e18
        if (wethUsdPrice18 == 0) return 0;
        // USD = WETH * (USD/WETH)
        return FullMath.mulDiv(wethAmount18, wethUsdPrice18, WAD);
    }
}

abstract contract SnapshotHelpers is Utils, ImEtfFactory  {

    function _snapshotVault(Etf memory etf, address etfVault)
        internal
        view
        returns (Snapshot[] memory s, uint256 etfTvlWeth)
    {
        uint256 n = etf.assets.length;
        s = new Snapshot[](n);

        IUniswapOracle oracle = getUniswapOracle();

        // 1) Fill token info and values, and accumulate TVL
        unchecked {
            for (uint256 i = 0; i < n; ++i) {
                address t = etf.assets[i].tokenAddress;
                uint8   d = IERC20Metadata(t).decimals();
                uint256 bal = IERC20(t).balanceOf(etfVault);
                uint256 p   = oracle.getTokenWethPrice(t); // WETH per 1 token, 1e18

                // tokenValueWeth18 = bal * price / 10^dec
                uint256 v = (bal == 0 || p == 0) ? 0 : FullMath.mulDiv(bal, p, 10 ** d);

                s[i] = Snapshot({
                    tokenAddress:     t,
                    tokenDecimals:    d,
                    currentWeight:    0,          // set in pass #2
                    tokenRawBalance:  bal,
                    tokenPriceWeth18: p,
                    tokenValueWeth18: v
                });

                etfTvlWeth += v;
            }
        }

        // 2) Compute live weights as value share of TVL (in bips)
        if (etfTvlWeth > 0) {
            unchecked {
                for (uint256 i = 0; i < n; ++i) {
                    s[i].currentWeight = uint16(
                        (s[i].tokenValueWeth18 * HUNDRED_PERCENT_BIPS) / etfTvlWeth
                    );
                }
            }
        }
    }

    /// @dev USD-enhanced snapshot: and adds USD prices/values using the WETH->USD oracle conversion.
    function _snapshotVaultWithUsd(Etf memory etf, address etfVault)
        internal
        view
        returns (SnapshotWithUsd[] memory s, uint256 etfTvlWeth, uint256 etfTvlUsd)
    {
        (Snapshot[] memory snap, uint256 tvlWeth) = _snapshotVault(etf, etfVault);

        etfTvlWeth = tvlWeth;
        etfTvlUsd  = _convertWethToUsd(tvlWeth);

        uint256 n = snap.length;
        s = new SnapshotWithUsd[](n);

        unchecked {
            for (uint256 i = 0; i < n; ++i) {
                s[i] = SnapshotWithUsd({
                    tokenAddress:      snap[i].tokenAddress,
                    tokenDecimals:     snap[i].tokenDecimals,
                    currentWeight:     snap[i].currentWeight,
                    tokenRawBalance:   snap[i].tokenRawBalance,
                    tokenPriceWeth18:  snap[i].tokenPriceWeth18,
                    tokenPriceUsd18:   _convertWethToUsd(snap[i].tokenPriceWeth18),
                    tokenValueWeth18:  snap[i].tokenValueWeth18,
                    tokenValueUsd18:   _convertWethToUsd(snap[i].tokenValueWeth18)
                });
            }
        }
    }
}

abstract contract PriceHelpers is SnapshotHelpers {
    /// @dev Target-weighted (index) price in WETH (1e18).
    function _getEtfInitialPrice(Etf memory etf) internal view returns (uint256 priceWeth18) {
        uint256 sum; // (bips * 1e18)
        IUniswapOracle oracle = getUniswapOracle();

        unchecked {
            for (uint256 i = 0; i < etf.assets.length; ++i) {
                address token = etf.assets[i].tokenAddress;
                uint16  wBips = etf.assets[i].weightBips;

                // 1e18-scaled: WETH per 1 whole TOKEN
                uint256 p = oracle.getTokenWethPrice(token);
                if (p == 0 || wBips == 0) continue;

                sum += uint256(wBips) * p;
            }
        }
        // Divide by 100% in bips → 1e18 WETH per "index unit"
        return sum / HUNDRED_PERCENT_BIPS;
    }

    /// @dev Live NAV per whole ETF token in WETH (1e18):
    ///      ( Σ_i [ vaultBalance_i (raw) * price_i (WETH per whole token, 1e18) / 10^dec_i ] )
    ///      / ( ETF totalSupply (whole tokens) )
    function _getEtfWethPrice(uint256 etfId, Etf memory etf, address etfVault) internal view returns (uint256) {
        // Resolve addresses
        address etfToken = getEtfFactory().getEtfTokenAddress(etfId);

        // Supply in whole-token terms
        uint256 rawSupply = IERC20(etfToken).totalSupply();
        uint8   etfDec   = IERC20Metadata(etfToken).decimals();
        if (rawSupply == 0) {
            // No supply yet → use index price
            return _getEtfInitialPrice(etf);
        }
        uint256 supplyWhole18 = _scaleTo1e18(rawSupply, etfDec); // #whole ETF tokens, 1e18-scaled
        if (supplyWhole18 == 0) return 0;

        // Sum vault holdings valued in WETH (1e18)
        uint256 totalWethValue18 = _getEtfTvlWeth(etf, etfVault);

        // NAV per whole ETF token (1e18) = totalWethValue18 / (#whole ETF tokens)
        return FullMath.mulDiv(totalWethValue18, WAD, supplyWhole18);
    }

    /// @dev Total vault value in WETH (1e18) across all ETF assets.
    function _getEtfTvlWeth(Etf memory etf, address etfVault) internal view returns (uint256 totalWethValue18) {
        IUniswapOracle oracle = getUniswapOracle();

        unchecked {
            // Sum vault holdings valued in WETH (1e18)
            for (uint256 i = 0; i < etf.assets.length; ++i) {
                address token = etf.assets[i].tokenAddress;

                uint256 bal = IERC20(token).balanceOf(etfVault); // raw units
                if (bal == 0) continue;

                // WETH per 1 whole token (1e18)
                uint256 wethPerToken18 = oracle.getTokenWethPrice(token);
                if (wethPerToken18 == 0) continue;

                // value = bal(raw) * price(1e18) / 10^dec
                uint8 dec = IERC20Metadata(token).decimals();
                uint256 denom = _pow10(dec);

                totalWethValue18 += FullMath.mulDiv(bal, wethPerToken18, denom);
            }
        }
    }
    
}

abstract contract EtfStats is PriceHelpers {

    struct EthUsd {
        uint256 eth18; // value in WETH (1e18)
        uint256 usd18; // value in USD  (1e18)
    }
    
    struct Stats {
        uint256 assetsLiquidityUsd;  // sum of TOKEN–WETH pool liquidity (USD, 1e18)
        uint256 assetsMcapUsd;       // sum of token market caps (USD, 1e18)
    }

    /// @dev Generic helper to sum a per-token USD metric across all ETF assets.
    ///      Accepts a function pointer of type (address) → (uint256).
    ///      Wrapped in try/catch so missing pools won’t revert.
    function _sumPerAsset(Etf memory etf, function (address) external view returns (uint256) fn) internal view returns (uint256 sum) {
        unchecked {
            for (uint256 i = 0; i < etf.assets.length; ++i) {
                address token = etf.assets[i].tokenAddress;
                try fn(token) returns (uint256 value) {
                    sum += value;
                } catch { /* ignore if oracle call fails */ }
            }
        }
    }

    /// @notice Returns full ETF stats.
    function _getStats(Etf memory etf) internal view returns (Stats memory s) {
        IUniswapOracle uniO = getUniswapOracle();

        s.assetsLiquidityUsd = _sumPerAsset(etf, uniO.getTokenLiquidityUsd);
        s.assetsMcapUsd      = _sumPerAsset(etf, uniO.getTokenMarketCapUsd);
    }
}


contract EtfOracle is ImDirectory, PriceHelpers, EtfStats {

    constructor(address _directory) ImDirectory(_directory) {}

    /// @notice Current NAV per ETF token in WETH (1e18).
    function getEtfWethPrice(uint256 etfId) external view returns (uint256) {
        (Etf memory etf, address vaultAddress) = getEtfFactory().getEtfByIdAndVault(etfId);
        return _getEtfWethPrice(etfId, etf, vaultAddress);
    }

    /// @notice Current NAV per ETF token in USD (1e18).
    /// @dev Uses the internal _convertWethToUsd helper for conversion.
    function getEtfUsdPrice(uint256 etfId) external view returns (uint256) {
        (Etf memory etf, address vaultAddress) = getEtfFactory().getEtfByIdAndVault(etfId);
        uint256 etfWethPrice18 = _getEtfWethPrice(etfId, etf, vaultAddress);
        return _convertWethToUsd(etfWethPrice18);
    }

    function getEtfPrice(uint256 etfId) external view returns (uint256 wethPrice18, uint256 usdPrice18) {
        (Etf memory etf, address vaultAddress) = getEtfFactory().getEtfByIdAndVault(etfId);
        wethPrice18 = _getEtfWethPrice(etfId, etf, vaultAddress);
        usdPrice18 = _convertWethToUsd(wethPrice18);
    }

    function snapshotVault(uint256 etfId) external view returns (Snapshot[] memory s, uint256 etfTvlWeth) {
        (Etf memory etf, address vaultAddress) = getEtfFactory().getEtfByIdAndVault(etfId);
        return _snapshotVault(etf, vaultAddress);
    }

    function snapshotVaultUnchecked(Etf memory etf, address vaultAddress) external view returns (Snapshot[] memory s, uint256 etfTvlWeth) {
        return _snapshotVault(etf, vaultAddress);
    }

    function snapshotVaultWithUsd(uint256 etfId) external view returns (SnapshotWithUsd[] memory s, uint256 etfTvlWeth, uint256 etfTvlUsd) {
        (Etf memory etf, address vaultAddress) = getEtfFactory().getEtfByIdAndVault(etfId);
        return _snapshotVaultWithUsd(etf, vaultAddress);
    }

    function getEtfStats(uint256 etfId) external view returns (Stats memory s) {
        (Etf memory etf) = getEtfFactory().getEtfById(etfId);
        return _getStats(etf);
    }
}"
    },
    "hopium/etf/interface/iEtfOracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/etf/types/etf.sol";
import "hopium/etf/types/snapshot.sol";

interface IEtfOracle {
   function getEtfWethPrice(uint256 etfId) external view returns (uint256);
   function getEtfPrice(uint256 etfId) external view returns (uint256 wethPrice18, uint256 usdPrice18);
   function snapshotVaultUnchecked(Etf memory etf, address vaultAddress) external view returns (Snapshot[] memory s, uint256 etfTvlWeth);
}"
    },
    "hopium/common/interface-ext/iErc20Metadata.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/common/interface-ext/iErc20.sol";

interface IERC20Metadata is IERC20 {
    function decimals() external view returns (uint8);
}"
    },
    "hopium/common/lib/full-math.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

library FullMath {
    /// @dev Full-precision multiply-divide: floor(a * b / denominator)
    ///      Reverts if denominator == 0 or the result overflows uint256.
    /// @notice Based on Uniswap v3’s FullMath.mulDiv().
    function mulDiv(
        uint256 a,
        uint256 b,
        uint256 denominator
    ) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            uint256 prod0; // Least-significant 256 bits
            uint256 prod1; // Most-significant 256 bits
            assembly {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // No overflow: do simple division
            if (prod1 == 0) return prod0 / denominator;

            require(denominator > prod1, "mulDiv overflow");

            ///////////////////////////////////////////////
            //  Make division exact  (subtract remainder)
            ///////////////////////////////////////////////
            uint256 remainder;
            assembly {
                remainder := mulmod(a, b, denominator)
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            ///////////////////////////////////////////////
            //  Factor powers of two out of denominator
            ///////////////////////////////////////////////
            uint256 twos = denominator & (~denominator + 1);
            assembly {
                denominator := div(denominator, twos)
                prod0 := div(prod0, twos)
                twos := add(div(sub(0, twos), twos), 1)
            }

            // Combine high and low products
            prod0 |= prod1 * twos;

            ///////////////////////////////////////////////
            //  Compute modular inverse of denominator mod 2²⁵⁶
            ///////////////////////////////////////////////
            uint256 inv = (3 * denominator) ^ 2;
            inv *= 2 - denominator * inv; // inverse mod 2⁸
            inv *= 2 - denominator * inv; // mod 2¹⁶
            inv *= 2 - denominator * inv; // mod 2³²
            inv *= 2 - denominator * inv; // mod 2⁶⁴
            inv *= 2 - denominator * inv; // mod 2¹²⁸
            inv *= 2 - denominator * inv; // mod 2²⁵⁶

            ///////////////////////////////////////////////
            //  Multiply by modular inverse to finish division
            ///////////////////////////////////////////////
            result = prod0 * inv;
            return result;
        }
    }
}"
    },
    "hopium/common/types/bips.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

uint256 constant HUNDRED_PERCENT_BIPS = 10_000;
"
    },
    "hopium/uniswap/interface/imUniswapOracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/common/interface/imDirectory.sol";
import "hopium/uniswap/interface/iUniswapOracle.sol";

abstract contract ImUniswapOracle is ImDirectory {

    function getUniswapOracle() internal view virtual returns (IUniswapOracle) {
        return IUniswapOracle(fetchFromDirectory("uniswap-oracle"));
    }

    modifier onlyUniswapOracle() {
        require(msg.sender == fetchFromDirectory("uniswap-oracle"), "msg.sender is not uniswap-oracle");
        _;
    }
}"
    },
    "hopium/etf/interface/imEtfFactory.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/common/interface/imDirectory.sol";
import "hopium/etf/types/etf.sol";

interface IEtfFactory {
    function updateEtfVolume(uint256 etfId, uint256 ethAmount) external;

    //read
    //data
    function getEtfById(uint256 etfId) external view returns (Etf memory);
    function getEtfTokenAddress(uint256 etfId) external view returns (address);
    function getEtfVaultAddress(uint256 etfId) external view returns (address);
    function getEtfByIdAndAddresses(uint256 etfId) external view returns (Etf memory etf, address tokenAddress, address vaultAddress);
    function getEtfByIdAndVault(uint256 etfId) external view returns (Etf memory etf, address vaultAddress);

    //events
    function emitVaultBalanceEvent(uint256 etfId) external;
    function emitPlatformFeeTransferredEvent(uint256 etfId, uint256 ethAmount) external;
}

abstract contract ImEtfFactory is ImDirectory {

    function getEtfFactory() internal view virtual returns (IEtfFactory) {
        return IEtfFactory(fetchFromDirectory("etf-factory"));
    }

    modifier onlyEtfFactory() {
        require(msg.sender == fetchFromDirectory("etf-factory"), "msg.sender is not etf factory");
        _;
    }
}"
    },
    "hopium/etf/types/etf.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

struct Asset {
    address tokenAddress;
    uint16 weightBips;
}

struct Etf {
    string name;
    string ticker;
    Asset[] assets;
}
"
    },
    "hopium/common/interface/imDirectory.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

/// @notice Interface used by the registry to talk to the external directory.
interface IDirectory {
    function owner() external view returns (address);
    function fetchFromDirectory(string memory _key) external view returns (address);
}

abstract contract ImDirectory {
    IDirectory public Directory;

    constructor(address _directory) {
        _setDirectory(_directory); // no modifier here
    }

    function changeDirectoryAddress(address _directory) external onlyOwner {
        _setDirectory(_directory);
    }

    function _setDirectory(address _directory) internal {
        require(_directory != address(0), "Directory cannot be zero address");
        require(_directory.code.length > 0, "Directory must be a contract");

        // Sanity check the interface
        try IDirectory(_directory).owner() returns (address) {
            Directory = IDirectory(_directory);
        } catch {
            revert("Directory address does not implement owner()");
        }
    }

    modifier onlyOwner() {
        require(msg.sender == Directory.owner(), "Caller is not the owner");
        _;
    }

    function owner() public view returns (address) {
        return Directory.owner();
    }

    function fetchFromDirectory(string memory _key) public view returns (address) {
        return Directory.fetchFromDirectory(_key);
    }
}
"
    },
    "hopium/uniswap/interface/iUniswapOracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

interface IUniswapOracle {
    function getTokenWethPrice(address tokenAddress) external view returns (uint256 price18);
    function getTokenUsdPrice(address tokenAddress) external view returns (uint256 price18);
    function getWethUsdPrice() external view returns (uint256 price18);
    function getTokenLiquidityWeth(address) external view returns (uint256);
    function getTokenLiquidityUsd(address) external view returns (uint256);
    function getTokenMarketCapWeth(address) external view returns (uint256);
    function getTokenMarketCapUsd(address) external view returns (uint256);
    function getTokenWethPriceByPool(address tokenAddress, address poolAddress, bool isV3Pool) external view returns (uint256);
    function emitPoolChangedEventOnWethUsdPool() external;
}
"
    },
    "hopium/etf/types/snapshot.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

struct Snapshot {
    address tokenAddress;
    uint8   tokenDecimals;
    uint16  currentWeight;
    uint256 tokenRawBalance;          // vault raw balance
    uint256 tokenPriceWeth18;  // WETH per 1 token (1e18)
    uint256 tokenValueWeth18;  // raw * price / 10^dec
}

struct SnapshotWithUsd {
    address tokenAddress;
    uint8   tokenDecimals;
    uint16  currentWeight;
    uint256 tokenRawBalance;          // vault raw balance
    uint256 tokenPriceWeth18;  // WETH per 1 token (1e18)
    uint256 tokenPriceUsd18;
    uint256 tokenValueWeth18;  // raw * price / 10^dec
    uint256 tokenValueUsd18;
}"
    },
    "hopium/common/interface-ext/iErc20.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
ERC20, DeFi, Liquidity, Factory, Oracle|addr:0xdd834a9b5263504f738da3e44e40ba76effa7019|verified:true|block:23668314|tx:0x8b976e591a1a88a452acb442fef7287316f34da7ab292f282c5375e2a898adb8|first_check:1761567304

Submitted on: 2025-10-27 13:15:05

Comments

Log in to comment.

No comments yet.