EtfFactory

Description:

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

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "hopium/etf/main/etf-factory.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/uniswap/interface/imPoolFinder.sol";
import "hopium/common/types/bips.sol";
import "hopium/common/lib/ididx.sol";
import "hopium/etf/interface/imEtfTokenDeployer.sol";
import "hopium/etf/interface/imEtfVaultDeployer.sol";
import "hopium/etf/interface/imEtfRouter.sol";
import "hopium/uniswap/interface/imUniswapOracle.sol";
import "hopium/etf/interface/imEtfRouter.sol";
import "hopium/common/lib/transfer-helpers.sol";

abstract contract Storage {
    uint256 internal immutable WAD = 1e18;
    address public immutable WETH_ADDRESS;

    Etf[] internal createdEtfs;

    mapping(bytes32 => uint256) internal etfTickerHashToId;
    mapping(bytes32 => uint256) internal etfAssetsHashToId;

    mapping(uint256 => address) internal etfIdToEtfTokens;
    mapping(uint256 => address) internal etfIdToEtfVaults;

    event EtfDeployed(uint256 indexed etfId, Etf etf, address etfTokenAddress, address etfVaultAddress);

    struct TokenBalance {
        address tokenAddress;
        uint256 tokenAmount;
    }

    event VaultBalanceChanged(uint256 etfId, TokenBalance[] updatedBalances);
    event PlatformFeeTransferred(uint256 etfId, uint256 ethAmount, uint256 usdValue);
}

/// @notice Validation + helpers fused for fewer passes & less memory churn
abstract contract EtfCreationHelpers is Storage, ImPoolFinder {
    /// @dev insertion sort by (tokenAddress, weightBips). Cheaper for small n.
    function _insertionSort(Asset[] memory a) internal pure {
        uint256 n = a.length;
        for (uint256 i = 1; i < n; ) {
            Asset memory key = a[i];
            uint256 j = i;
            while (j > 0) {
                Asset memory prev = a[j - 1];
                // order by address, then by weight
                if (prev.tokenAddress < key.tokenAddress || 
                   (prev.tokenAddress == key.tokenAddress && prev.weightBips <= key.weightBips)) {
                    break;
                }
                a[j] = prev;
                unchecked { --j; }
            }
            a[j] = key;
            unchecked { ++i; }
        }
    }

    /// @dev canonical, order-insensitive hash for token-weight pairs from a *sorted* array
    function _computeEtfAssetsHashSorted(Asset[] memory sorted) internal pure returns (bytes32) {
        // Avoid building parallel arrays; encode the struct array directly
        return keccak256(abi.encode(sorted));
    }

    /// @dev derive bytes32 key for ticker
    function _tickerKey(string calldata t) internal pure returns (bytes32) {
        bytes memory b = bytes(t); // calldata -> memory copy
        uint256 len = b.length;
        for (uint256 i = 0; i < len; ) {
            uint8 c = uint8(b[i]);
            // 'A'..'Z' -> 'a'..'z'
            if (c >= 65 && c <= 90) {
                b[i] = bytes1(c + 32);
            }
            unchecked { ++i; }
        }
        return keccak256(b);
    }

    error EmptyTicker();
    error TickerExists();
    /// @dev ensure ticker is non-empty and globally unique (by bytes32 hash)
    function _validateTicker(string calldata ticker) internal view returns (bytes32 tickerHash) {
        if (bytes(ticker).length == 0) revert EmptyTicker();
        tickerHash = _tickerKey(ticker);
        if (etfTickerHashToId[tickerHash] != 0) revert TickerExists();
    }

    error NoAssets();
    error ZeroToken();
    error ZeroWeight();
    error OverWeight();
    error NotHundred();
    error DuplicateToken();
    error NoPoolFound();
    /// @dev copy->sort once; validate weights/duplicates in a single linear pass
    function _validateAndSort(Asset[] calldata assets)
        internal
        returns (Asset[] memory sorted)
    {
        uint256 n = assets.length;
        if (n == 0) revert NoAssets();

        // Copy calldata to memory once
        sorted = new Asset[](n);
        for (uint256 i = 0; i < n; ) {
            Asset calldata a = assets[i];
            address token = a.tokenAddress;
            uint256 w = a.weightBips;

            if (token != WETH_ADDRESS) {
                Pool memory pool = getPoolFinder().getBestWethPoolAndForceUpdate(token);
                if (pool.poolAddress == address(0)) revert NoPoolFound();
            }

            if (token == address(0)) revert ZeroToken();
            if (w == 0) revert ZeroWeight();
            if (w > HUNDRED_PERCENT_BIPS) revert OverWeight();

            sorted[i] = a;
            unchecked { ++i; }
        }

        // Sort in-place
        _insertionSort(sorted);

        // Single pass: check duplicates + sum to 100%
        uint256 sum;
        for (uint256 i = 0; i < n; ) {
            if (i > 0 && sorted[i - 1].tokenAddress == sorted[i].tokenAddress) {
                revert DuplicateToken();
            }
            sum += sorted[i].weightBips;
            unchecked { ++i; }
        }
        if (sum != HUNDRED_PERCENT_BIPS) revert NotHundred();
    }

    error EtfExists();
    /// @dev ensure the set (by canonical hash) hasn't been used yet
    function _validateEtfAssetsHashUnique(Asset[] memory sorted) internal view returns (bytes32 etfAssetsHash) {
        etfAssetsHash = _computeEtfAssetsHashSorted(sorted);
        if (etfAssetsHashToId[etfAssetsHash] != 0) revert EtfExists();
    }
}

abstract contract Helpers is EtfCreationHelpers, ImUniswapOracle, ImEtfRouter  {
    function _createEtf(Etf calldata etf) internal returns (uint256) {
        // Validate ticker & get key
        bytes32 tickerHash = _validateTicker(etf.ticker);

        // Validate holdings (copy, sort once, linear checks)
        Asset[] memory sorted = _validateAndSort(etf.assets);

        // Unique by canonical hash
        bytes32 assetsHash = _validateEtfAssetsHashUnique(sorted);

        // next 0-based position becomes a 1-based id via helper
        uint256 etfId = IdIdx.idxToId(createdEtfs.length);
        createdEtfs.push(etf);

        // register mappings
        etfTickerHashToId[tickerHash] = etfId;
        etfAssetsHashToId[assetsHash] = etfId;

        return etfId;
    }

    error ZeroWethUsdPrice();
    function _getSeedPrice() internal view returns (uint256 usdInEth) {
        uint256 wethUsd = getUniswapOracle().getWethUsdPrice(); // 1 ETH = wethUsd USD (1e18)
        if (wethUsd == 0) revert ZeroWethUsdPrice();
        // 1 USD = 1 / wethUsd ETH → scaled by WAD (1e18)
        usdInEth = (WAD * WAD) / wethUsd;
    }

    error InvalidSeedAmount();
    /// @notice Internal seeding helper. Mints ETF tokens equal to the ETH seed provided.
    function _seedEtf(uint256 etfId) internal {
        if (msg.value < _getSeedPrice()) revert InvalidSeedAmount();

        // Forward ETH to router which mints ETF tokens to `receiver`
        getEtfRouter().mintEtfTokens{ value: msg.value }(etfId, address(this), "");
    }

    error InvalidId();
    function _getEtfById(uint256 etfId) internal view returns (Etf memory) {
        if (etfId == 0 || etfId > createdEtfs.length) revert InvalidId();
        return createdEtfs[IdIdx.idToIdx(etfId)];
    }

    error InvalidAddress();
    function _getEtfTokenAddress(uint256 etfId) internal view returns (address tokenAddress) {
        tokenAddress = etfIdToEtfTokens[etfId];
        if (tokenAddress == address(0)) revert InvalidAddress();
    }

    function _getEtfVaultAddress(uint256 etfId) internal view returns (address vaultAddress) {
        vaultAddress = etfIdToEtfVaults[etfId];
        if (vaultAddress == address(0)) revert InvalidAddress();
    }
}

abstract contract VaultBalance is Helpers {
    function emitVaultBalanceEvent(uint256 etfId) public onlyEtfRouter {
        Etf memory etf = _getEtfById(etfId);
        address etfVault = _getEtfVaultAddress(etfId);

        uint256 n = etf.assets.length;
        TokenBalance[] memory updated = new TokenBalance[](n);

        // Snapshot each asset's ERC-20 balance held by the vault (raw units)
        for (uint256 i = 0; i < n; ) {
            address token = etf.assets[i].tokenAddress;
            uint256 bal   = IERC20(token).balanceOf(etfVault);

            updated[i] = TokenBalance({ tokenAddress: token, tokenAmount: bal });

            unchecked { ++i; }
        }

        emit VaultBalanceChanged(etfId, updated);
    }

    function emitPlatformFeeTransferredEvent(uint256 etfId, uint256 ethAmount) public onlyEtfRouter {
        uint256 wethUsdPrice = getUniswapOracle().getWethUsdPrice();
        uint256 usdAmount = ethAmount * wethUsdPrice / 1e18;

        emit PlatformFeeTransferred(etfId, ethAmount, usdAmount);
    }
}

contract EtfFactory is ImDirectory, VaultBalance, ImEtfTokenDeployer, ImEtfVaultDeployer {
    constructor(address _directory,  address _wethAddress) ImDirectory(_directory) {
        WETH_ADDRESS = _wethAddress;
    }

    // -- Write fns --

    function createEtf(Etf calldata etf) external payable onlyOwner returns (uint256) {
        //create etf
        uint256 etfId = _createEtf(etf);

        // deploy etf token
        address etfTokenAddress = getEtfTokenDeployer().deployEtfToken(etfId, etf.name, etf.ticker);

        // deploy etf vault
        address etfVaultAddress = getEtfVaultDeployer().deployEtfVault();

        etfIdToEtfTokens[etfId] = etfTokenAddress;
        etfIdToEtfVaults[etfId] = etfVaultAddress;

        emit EtfDeployed(etfId, etf, etfTokenAddress, etfVaultAddress);

        _seedEtf(etfId);

        if(etfId == 1) {
            getUniswapOracle().emitPoolChangedEventOnWethUsdPool();
        }

        return etfId;
    }

    // -- Read fns --
    function getEtfById(uint256 etfId) public view returns (Etf memory) {
        return _getEtfById(etfId);
    }

    function getEtfTokenAddress(uint256 etfId) public view returns (address tokenAddress) {
        tokenAddress = _getEtfTokenAddress(etfId);
    }

    function getEtfVaultAddress(uint256 etfId) public view returns (address vaultAddress) {
        vaultAddress = _getEtfVaultAddress(etfId);
    }

    function getEtfByIdAndAddresses(uint256 etfId) external view returns (Etf memory etf, address tokenAddress, address vaultAddress) {
        etf = getEtfById(etfId);
        tokenAddress = getEtfTokenAddress(etfId);
        vaultAddress = getEtfVaultAddress(etfId);
    }

    function getEtfByIdAndVault(uint256 etfId) external view returns (Etf memory etf, address vaultAddress) {
        etf = getEtfById(etfId);
        vaultAddress = getEtfVaultAddress(etfId);
    }

    function getSeedPrice() external view returns (uint256) {
        uint256 base = _getSeedPrice();                 // in ETH (1e18)
        uint256 num = HUNDRED_PERCENT_BIPS + 100;     // 11000 bips = +1%
        // ceil(base * 11000 / 10000)
        return (base * num + HUNDRED_PERCENT_BIPS - 1) / HUNDRED_PERCENT_BIPS;
    }

    function recoverAsset(address tokenAddress, address toAddress) public onlyOwner {
        TransferHelpers.recoverAsset(tokenAddress, toAddress);
    }

    receive() external payable {}
}"
    },
    "hopium/common/lib/transfer-helpers.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

library TransferHelpers {
    /* ----------------------------- Custom Errors ----------------------------- */
    error ZeroAmount();
    error InsufficientETH();
    error ETHSendFailed();
    error AllowanceTooLow();
    error NothingReceived();

    /* ----------------------------- Internal Helpers -------------------------- */

    function safeTransfer(
        address token,
        address to,
        uint256 value
    ) internal {
        (bool success, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED");
    }

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        (bool success, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAILED");
    }

    function safeApprove(address token, address spender, uint256 value) internal {
        (bool success, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), "APPROVE_FAILED");
    }

    /* ------------------------------- ETH helpers ----------------------------- */

    function sendEth(address to, uint256 amount) internal {
        if (amount == 0) revert ZeroAmount();
        if (address(this).balance < amount) revert InsufficientETH();

        (bool ok, ) = payable(to).call{value: amount}("");
        if (!ok) revert ETHSendFailed();
    }

    /* ------------------------------- WETH helpers ----------------------------- */

    function wrapETH(address wethAddress, uint256 amt) internal {
        if (address(this).balance < amt) revert InsufficientETH();
        IWETH(wethAddress).deposit{value: amt}();
    }

    error ETHSendFailure();
    function unwrapAndSendWETH(address wethAddress, uint256 amt, address payable to) internal {
        IWETH(wethAddress).withdraw(amt);
        (bool ok, ) = to.call{value: amt}("");
        if (!ok) revert ETHSendFailure();
    }

    /* ------------------------------ Token helpers ---------------------------- */

    function sendToken(address token, address to, uint256 amount) internal {
        if (amount == 0) revert ZeroAmount();
        safeTransfer(token, to, amount);
    }

    function receiveToken(address token, uint256 amount, address from) internal returns (uint256 received) {
        if (amount == 0) revert ZeroAmount();

        uint256 currentAllowance = IERC20(token).allowance(from, address(this));
        if (currentAllowance < amount) revert AllowanceTooLow();

        uint256 balBefore = IERC20(token).balanceOf(address(this));
        safeTransferFrom(token, from, address(this), amount);
        uint256 balAfter = IERC20(token).balanceOf(address(this));

        unchecked {
            received = balAfter - balBefore;
        }
        if (received == 0) revert NothingReceived();
    }

    function approveMaxIfNeeded(address token, address spender, uint256 amount) internal {
        uint256 current = IERC20(token).allowance(address(this), spender);
        if (current < amount) {
            if (current != 0) safeApprove(token, spender, 0);
            safeApprove(token, spender, type(uint256).max);
        }
    }

    /* ------------------------------ ETH + Token helpers ---------------------------- */

    function sendEthOrToken(address token, address to) internal {
        if (token == address(0)) {
            sendEth(to, address(this).balance);
        } else {
            uint256 bal = IERC20(token).balanceOf(address(this));
            if (bal > 0) safeTransfer(token, to, bal);
        }
    }

    function recoverAsset(address token, address to) internal {
        sendEthOrToken(token, to);
    }
}
"
    },
    "hopium/etf/interface/imEtfRouter.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IEtfRouter {
    function mintEtfTokens(uint256 etfId, address receiver, string calldata affiliateCode) external payable;
}

abstract contract ImEtfRouter is ImDirectory {

    function getEtfRouter() internal view virtual returns (IEtfRouter) {
        return IEtfRouter(fetchFromDirectory("etf-router"));
    }

    modifier onlyEtfRouter() {
        require(msg.sender == fetchFromDirectory("etf-router"), "msg.sender is not etf router");
        _;
    }
}"
    },
    "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/imEtfVaultDeployer.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IEtfVaultDeployer {
   function deployEtfVault() external returns (address);
}

abstract contract ImEtfVaultDeployer is ImDirectory {

    function getEtfVaultDeployer() internal view virtual returns (IEtfVaultDeployer) {
        return IEtfVaultDeployer(fetchFromDirectory("etf-vault-deployer"));
    }
    
}"
    },
    "hopium/etf/interface/imEtfTokenDeployer.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IEtfTokenDeployer {
    function deployEtfToken(uint256 etfId, string calldata name, string calldata symbol) external returns (address);
}

abstract contract ImEtfTokenDeployer is ImDirectory {

    function getEtfTokenDeployer() internal view virtual returns (IEtfTokenDeployer) {
        return IEtfTokenDeployer(fetchFromDirectory("etf-token-deployer"));
    }
    
}"
    },
    "hopium/common/lib/ididx.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

library IdIdx {

    function idxToId(uint256 idx) internal pure returns (uint256) {
        return idx + 1;
    }

    function idToIdx(uint256 id) internal pure returns (uint256) {
        return id - 1;
    }
}"
    },
    "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/imPoolFinder.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/uniswap/types/pool.sol";
import "hopium/common/interface/imDirectory.sol";

interface IPoolFinder {
    function getBestWethPool(address tokenAddress) external view returns (Pool memory pool);
    function getBestWethPoolAndUpdateIfStale(address tokenAddress) external returns (Pool memory pool);
    function getBestWethPoolAndForceUpdate(address tokenAddress) external returns (Pool memory out);
    function emitPoolChangedEventOnWethUsdPool(address poolAddress, bool isV3Pool) external;
}

abstract contract ImPoolFinder is ImDirectory {

    function getPoolFinder() internal view virtual returns (IPoolFinder) {
        return IPoolFinder(fetchFromDirectory("pool-finder"));
    }
}"
    },
    "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/uniswap/types/pool.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

struct Pool {
    address poolAddress; // 20
    uint24  poolFee;     // 3
    uint8   isV3Pool;    // 1 (0/1)
    uint64  lastCached;  // 8
}"
    },
    "hopium/common/interface-ext/iWeth.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256) external;
}"
    },
    "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, Factory, Oracle|addr:0x66f0190709515b9085dace355a0fd04f3cbb4ae7|verified:true|block:23684405|tx:0xe06a20da0c396050a0336710f2958994bdb72e629c957a0b716c4ccf6bb92fbf|first_check:1761758025

Submitted on: 2025-10-29 18:13:45

Comments

Log in to comment.

No comments yet.