EtfRouter

Description:

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

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

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

import "hopium/common/interface/imDirectory.sol";
import "hopium/etf/interface/imEtfFactory.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "hopium/uniswap/interface/imMultiSwapRouter.sol";
import "hopium/etf/interface/imEtfOracle.sol";
import "hopium/etf/interface/iEtfToken.sol";
import "hopium/common/types/bips.sol";
import "hopium/etf/interface/iEtfVault.sol";
import "hopium/common/lib/transfer-helpers.sol";
import "hopium/common/interface/imActive.sol";
import "hopium/uniswap/interface/imUniswapOracle.sol";
import "hopium/common/interface-ext/iErc20Metadata.sol";
import "hopium/common/lib/full-math.sol";
import "hopium/etf/lib/router-inputs.sol";
import "hopium/etf/interface/imEtfAffiliate.sol";

error ZeroAmount();
error ZeroReceiver();
error InvalidBips();

abstract contract Storage {
    uint16 public PLATFORM_FEE_BIPS = 50;
    uint16 public AFFILIATE_USER_DISCOUNT_PERC = 25;
    uint256 internal constant WAD = 1e18;
    uint32 public DEFAULT_SLIPPAGE_BIPS = 300;
}

abstract contract Fee is ImDirectory, Storage, ImEtfFactory, ImEtfAffiliate {
    /// @notice Transfers platform fee, optionally splitting with an affiliate.
    /// @dev
    ///  - If `affiliateCode` is empty or has no owner → 100% of fee to vault.
    ///  - If code has owner:
    ///       total fee = 75% of PLATFORM_FEE,
    ///       split 50% to vault and 50% to affiliate owner.
    /// @return netAmount ETH remaining after fee deduction.
    function _transferPlatformFee(
        uint256 etfId,
        uint256 amount,
        string calldata affiliateCode
    ) internal returns (uint256 netAmount) {
        address vault = fetchFromDirectory("vault");
        uint16 feeBips = PLATFORM_FEE_BIPS;
        if (vault == address(0) || feeBips == 0) return amount;

        IEtfAffiliate affiliate = getEtfAffiliate();

        address codeOwner;
        if (bytes(affiliateCode).length != 0) {
            codeOwner = affiliate.getAffiliateOwner(affiliateCode);
        }

        // Compute the base platform fee
        uint256 fee = (amount * uint256(feeBips)) / uint256(HUNDRED_PERCENT_BIPS);
        if (fee == 0) return amount;

        // --- Case 1: no affiliate owner ---
        if (codeOwner == address(0)) {
            TransferHelpers.sendEth(vault, fee);
            getEtfFactory().emitPlatformFeeTransferredEvent(etfId, fee);
            return amount - fee;
        }

        // --- Case 2: valid affiliate owner ---
        // charge only 75% of the normal fee
        uint256 discountedFee = (fee * (100 - AFFILIATE_USER_DISCOUNT_PERC)) / 100;
        // split 50/50 between vault and affiliate
        uint256 half = discountedFee / 2;

        TransferHelpers.sendEth(vault, half);
        TransferHelpers.sendEth(payable(codeOwner), half);
        affiliate.emitFeeTransferredEvent(affiliateCode, half);
        getEtfFactory().emitPlatformFeeTransferredEvent(etfId, half);

        return amount - discountedFee;
    }

    function _refundEthDust(address receiver) internal {
        // Return any dust ETH
        uint256 leftover = address(this).balance;
        if (leftover > 0) TransferHelpers.sendEth(receiver, leftover);
    }
}

abstract contract MultiSwapHelpers is Fee, ImMultiSwapRouter {
    function _transferTokensToMultiswapRouter(MultiTokenInput[] memory sells, address etfVaultAddress) internal {
        address routerAddress = address(getMultiSwapRouter());
        for (uint256 i = 0; i < sells.length; ) {
            IEtfVault(etfVaultAddress).redeem(
                sells[i].tokenAddress,
                sells[i].amount,
                routerAddress
            );
            unchecked { ++i; }
        }
    }

    error ZeroDelta();
    function _swapMultipleTokensToEth(MultiTokenInput[] memory sells, address etfVaultAddress) internal returns (uint256 ethRealised) {
        // Pre-transfer vault tokens to router
        _transferTokensToMultiswapRouter(sells, etfVaultAddress);

        // Balance snapshot
        uint256 ethBefore = address(this).balance;

        // Execute swaps -> ETH to this contract
        getMultiSwapRouter().swapMultipleTokensToEth(
            sells,
            payable(address(this)),
            DEFAULT_SLIPPAGE_BIPS,
            true
        );

        // Compute realized ETH
        uint256 ethAfter = address(this).balance;
        if (ethAfter <= ethBefore) revert ZeroDelta();
        
        ethRealised = ethAfter - ethBefore;
    }
}

abstract contract MintHelpers is ImEtfOracle, MultiSwapHelpers {

    function _calMintAmount(uint256 etfId, uint256 totalSupplyBefore, uint256 tvlBefore, uint256 ethRealised) internal view returns (uint256 mintAmount) {
        if (totalSupplyBefore == 0) {
            // Genesis: use oracle index NAV (WETH per ETF token, 1e18)
            uint256 priceWeth18 = getEtfOracle().getEtfWethPrice(etfId);
            if (priceWeth18 == 0) revert ZeroEtfPrice();
            // tokens = value / price
            mintAmount = FullMath.mulDiv(ethRealised, WAD, priceWeth18);
        } else {
            // Proportional mint at current NAV: minted = delta * supply0 / tvlBefore
            mintAmount = FullMath.mulDiv(ethRealised, totalSupplyBefore, tvlBefore);
        }
        if (mintAmount == 0) revert NoMintedAmount();
    }

    function _executeMintBuys(Etf memory etf, address etfVaultAddress, uint256 ethBudget) internal returns (uint256 tvlBefore_, uint256 ethRealised) {
        // Snapshot Before
        (Snapshot[] memory snapBefore, uint256 tvlBefore) = getEtfOracle().snapshotVaultUnchecked(etf, etfVaultAddress);

        //Build + execute swaps
        MultiTokenOutput[] memory buys = RouterInputs.buildMintOutputs(etf, ethBudget, snapBefore, tvlBefore);

        getMultiSwapRouter().swapEthToMultipleTokens{value: ethBudget}(buys, etfVaultAddress, DEFAULT_SLIPPAGE_BIPS);
        
        //Snapshot After
        (, uint256 tvlAfter) = getEtfOracle().snapshotVaultUnchecked(etf, etfVaultAddress);
       
        if (tvlAfter <= tvlBefore) revert DeltaError();
        ethRealised = tvlAfter - tvlBefore;
        tvlBefore_ = tvlBefore;
    }
    
    error ZeroEtfPrice();
    error NoMintedAmount();
    error DeltaError();
    function _mintEtfTokens(
        uint256 etfId,
        Etf memory etf,
        address etfTokenAddress,
        address etfVaultAddress,
        address receiver,
        string calldata affiliateCode
    ) internal returns (uint256 mintAmount, uint256 ethRealised_) {
        // Net ETH after platform fee
        uint256 ethBudget = _transferPlatformFee(etfId, msg.value, affiliateCode);
        
        // Snapshot totalSupply before
        uint256 totalSupplyBefore = IERC20(etfTokenAddress).totalSupply();

        // Buy tokens
        (uint256 tvlBefore, uint256 ethRealised) = _executeMintBuys(etf, etfVaultAddress, ethBudget);

        // Mint amount
        mintAmount = _calMintAmount(etfId, totalSupplyBefore, tvlBefore, ethRealised);

        // Mint to receiver
        IEtfToken(etfTokenAddress).mint(receiver, mintAmount);
        ethRealised_ = ethRealised;
    }

}

abstract contract RedeemHelpers is MintHelpers {

    function _executeRedeemSells(uint256 etfId, Etf memory etf, uint256 etfTokenAmount, address etfVaultAddress) internal returns (uint256 ethRealised) {
        uint256 priceWeth18 = getEtfOracle().getEtfWethPrice(etfId);
        if (priceWeth18 == 0) revert ZeroEtfPrice();

        uint256 targetEth = FullMath.mulDiv(etfTokenAmount, priceWeth18, WAD);

        (Snapshot[] memory snap, uint256 tvlWeth18) = getEtfOracle().snapshotVaultUnchecked(etf, etfVaultAddress);

        // Build sells
        MultiTokenInput[] memory sells = RouterInputs.buildRedeemInputs(targetEth, snap, tvlWeth18);

        //Execute sells
        ethRealised = _swapMultipleTokensToEth(sells, etfVaultAddress);
    } 

    error SupplyZero();
    function _redeemEtfTokens(
        uint256 etfId,
        Etf memory etf,
        address etfTokenAddress,
        address etfVaultAddress,
        uint256 etfTokenAmount,
        address payable receiver,
        string calldata affiliateCode
    ) internal returns (uint256) {
        // Sell tokens
        uint256 ethRealised = _executeRedeemSells(etfId, etf, etfTokenAmount, etfVaultAddress);

        // Platform fee & payout
        uint256 ethFinal = _transferPlatformFee(etfId, ethRealised, affiliateCode);

        //Send final Eth to Receiver
        TransferHelpers.sendEth(receiver, ethFinal);

        // Burn ETF tokens
        IEtfToken(etfTokenAddress).burn(msg.sender, etfTokenAmount);

        return ethFinal;
    }
}

abstract contract RebalanceHelpers is RedeemHelpers {

    function _rebalance(Etf memory etf, address etfVaultAddress) internal {
        // Build sell inputs
        (Snapshot[] memory snapBefore, uint256 tvlBefore) = getEtfOracle().snapshotVaultUnchecked(etf, etfVaultAddress);
        (MultiTokenInput[] memory sells,) = RouterInputs.buildRebalanceRedeemInputs(etf, snapBefore, tvlBefore);

        // Execute sells
        uint256 ethRealised = _swapMultipleTokensToEth(sells, etfVaultAddress);

        // Recompute snapshot and buy underweights
        (Snapshot[] memory snapMid, uint256 tvlMid) = getEtfOracle().snapshotVaultUnchecked(etf, etfVaultAddress);
        MultiTokenOutput[] memory buys = RouterInputs.buildMintOutputs(etf, ethRealised, snapMid, tvlMid);

        // Execute buys (ETH → vault tokens)
        getMultiSwapRouter().swapEthToMultipleTokens{ value: ethRealised }(
            buys,
            etfVaultAddress,
            DEFAULT_SLIPPAGE_BIPS
        );
    }
}

contract EtfRouter is ReentrancyGuard, ImActive, RebalanceHelpers  {
    constructor(address _directory) ImDirectory(_directory) {}

    function mintEtfTokens(uint256 etfId, address receiver, string calldata affiliateCode) external payable nonReentrant onlyActive {
        if (msg.value == 0) revert ZeroAmount();
        if (receiver == address(0)) revert ZeroReceiver();

        // Resolve config
        (Etf memory etf, address etfToken, address etfVault) = getEtfFactory().getEtfByIdAndAddresses(etfId);

        _mintEtfTokens(etfId, etf, etfToken, etfVault, receiver, affiliateCode);

        _refundEthDust(msg.sender);

        getEtfFactory().emitVaultBalanceEvent(etfId);
    }

    // -------- Redeem to ETH --------
    function redeemEtfTokens(uint256 etfId, uint256 etfTokenAmount, address payable receiver, string calldata affiliateCode) external nonReentrant onlyActive {
        if (etfTokenAmount == 0) revert ZeroAmount();
        if (receiver == address(0)) revert ZeroReceiver();

        (Etf memory etf, address etfTokenAddress, address etfVaultAddress) = getEtfFactory().getEtfByIdAndAddresses(etfId);

        _redeemEtfTokens(etfId, etf, etfTokenAddress, etfVaultAddress, etfTokenAmount, receiver, affiliateCode);

        getEtfFactory().emitVaultBalanceEvent(etfId);
    }

    function rebalance(uint256 etfId) external nonReentrant onlyActive {
        (Etf memory etf, address etfVaultAddress) = getEtfFactory().getEtfByIdAndVault(etfId);
        _rebalance(etf, etfVaultAddress);
       _refundEthDust(msg.sender);

        getEtfFactory().emitVaultBalanceEvent(etfId);
    }

    error InvalidPerc();
    function changePlatformFee(uint16 newFeeBips, uint16 affUserDiscountPerc) public onlyOwner onlyActive {
        if (newFeeBips > HUNDRED_PERCENT_BIPS) revert InvalidBips();
        if (affUserDiscountPerc > 100) revert InvalidPerc();
        PLATFORM_FEE_BIPS = newFeeBips;
        AFFILIATE_USER_DISCOUNT_PERC = affUserDiscountPerc;
    }

     function changeDefaultSlippage(uint32 newSlippageBips) external onlyOwner {
        if (newSlippageBips > HUNDRED_PERCENT_BIPS) revert InvalidBips();
        DEFAULT_SLIPPAGE_BIPS = newSlippageBips;
    }

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

    receive() external payable {}
}"
    },
    "hopium/etf/interface/imEtfAffiliate.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IEtfAffiliate {
    function getAffiliateOwner(string calldata code) external view returns (address);
    function emitFeeTransferredEvent(string calldata code, uint256 ethAmount) external;
}

abstract contract ImEtfAffiliate is ImDirectory {

    function getEtfAffiliate() internal view virtual returns (IEtfAffiliate) {
        return IEtfAffiliate(fetchFromDirectory("etf-affiliate"));
    }

}"
    },
    "hopium/etf/lib/router-inputs.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/etf/types/etf.sol";
import "hopium/etf/types/snapshot.sol";
import "hopium/uniswap/types/multiswap.sol";
import "hopium/common/types/bips.sol";
import "hopium/etf/lib/hamilton.sol";
import "hopium/common/lib/full-math.sol";

library RouterInputs {
    //----- Mint -----
    function buildMintOutputs(
        Etf memory etf,
        uint256 ethBudget,
        Snapshot[] memory snap,
        uint256 tvlWeth18
    ) internal pure returns (MultiTokenOutput[] memory buys) {
        uint256 n = etf.assets.length;
        uint256 postMintTVLWeth18 = tvlWeth18 + ethBudget;

        // Collect underweights (value deficits in WETH18)
        address[] memory uwTok = new address[](n);
        uint256[] memory uwDef = new uint256[](n);
        uint256 m;
        for (uint256 i = 0; i < n; ) {
            uint256 target = (uint256(etf.assets[i].weightBips) * postMintTVLWeth18) / HUNDRED_PERCENT_BIPS;
            uint256 cur = snap[i].tokenValueWeth18;
            if (target > cur) {
                uwTok[m] = snap[i].tokenAddress;
                uwDef[m] = target - cur;
                unchecked { ++m; }
            }
            unchecked { ++i; }
        }

        if (m == 0) {
            // Fallback: normalize target weights to 10_000 bips
            uint256[] memory numerators = new uint256[](n);
            for (uint256 i = 0; i < n; ) { numerators[i] = etf.assets[i].weightBips; unchecked { ++i; } }

            // Produce normalized weights
            uint256[] memory w = Hamilton.distribute(
                numerators,
                HUNDRED_PERCENT_BIPS,
                new uint256[](0)
            );

            buys = new MultiTokenOutput[](n);
            for (uint256 i = 0; i < n; ) {
                buys[i] = MultiTokenOutput({
                    tokenAddress: snap[i].tokenAddress,
                    weightBips: uint16(w[i])  // fits since total is 10_000
                });
                unchecked { ++i; }
            }
            return buys;
        }

        // Allocate 10,000 bips proportionally to deficits
        uint256[] memory nums = new uint256[](m);
        for (uint256 i = 0; i < m; ) { nums[i] = uwDef[i]; unchecked { ++i; } }

        // Produce normalized weights for underweights only
        uint256[] memory w2 = Hamilton.distribute(
            nums,
            HUNDRED_PERCENT_BIPS,
            new uint256[](0)
        );

        buys = new MultiTokenOutput[](m);
        for (uint256 i = 0; i < m; ) {
            buys[i] = MultiTokenOutput({
                tokenAddress: uwTok[i],
                weightBips: uint16(w2[i])
            });
            unchecked { ++i; }
        }
    }

    //----- Redeem -----
    error ZeroTvl();
    error TargetGtTvl();
    error EmptyArr();
    function buildRedeemInputs(
        uint256 targetEthOut,
        Snapshot[] memory snap,
        uint256 tvlWeth18               // now used
    ) internal pure returns (MultiTokenInput[] memory sells) {
        uint256 n = snap.length;

        // 1) Use provided TVL
        if (tvlWeth18 == 0) revert ZeroTvl();
        if (targetEthOut > tvlWeth18) revert TargetGtTvl();

        // 2) Pro-rata based on snapshot composition, divide by tvlWeth18
        MultiTokenInput[] memory tmp = new MultiTokenInput[](n);
        uint256 count;
        for (uint256 i = 0; i < n; ) {
            uint256 p   = snap[i].tokenPriceWeth18;
            uint256 bal = snap[i].tokenRawBalance;
            if (p != 0 && bal != 0) {
                uint256 targetVal = FullMath.mulDiv(
                    targetEthOut,
                    snap[i].tokenValueWeth18,
                    tvlWeth18
                );

                uint256 raw = FullMath.mulDiv(
                    targetVal,
                    10 ** snap[i].tokenDecimals,
                    p
                );
                if (raw > bal) raw = bal;

                if (raw != 0) {
                    tmp[count++] = MultiTokenInput({
                        tokenAddress: snap[i].tokenAddress,
                        amount: raw
                    });
                }
            }
            unchecked { ++i; }
        }
        if (count == 0) revert EmptyArr();

        sells = new MultiTokenInput[](count);
        for (uint256 i = 0; i < count; ) {
            sells[i] = tmp[i];
            unchecked { ++i; }
        }
    }


    function buildRebalanceRedeemInputs(
        Etf memory etf, 
        Snapshot[] memory snapBefore, 
        uint256 tvlBefore
    ) internal pure returns (MultiTokenInput[] memory sells, uint256 sellCount) {
        // -------------------- Snapshot current vault --------------------
        if (tvlBefore == 0) revert ZeroTvl(); // no TVL → nothing to rebalance

        uint256 n = etf.assets.length;
        uint256[] memory targets = new uint256[](n);
        uint256[] memory currentVals = new uint256[](n);

        // -------------------- Compute target vs. actual --------------------
        for (uint256 i = 0; i < n; ) {
            targets[i] = (uint256(etf.assets[i].weightBips) * tvlBefore) / HUNDRED_PERCENT_BIPS;
            currentVals[i] = snapBefore[i].tokenValueWeth18;
            unchecked { ++i; }
        }

        // -------------------- Identify overweights to sell --------------------
        sells = new MultiTokenInput[](n);

        for (uint256 i = 0; i < n; ) {
            if (currentVals[i] > targets[i]) {
                uint256 excessValue = currentVals[i] - targets[i];
                uint256 rawAmount = FullMath.mulDiv(
                    excessValue,
                    10 ** snapBefore[i].tokenDecimals,
                    snapBefore[i].tokenPriceWeth18
                );
                if (rawAmount > 0) {
                    sells[sellCount++] = MultiTokenInput({
                        tokenAddress: snapBefore[i].tokenAddress,
                        amount: rawAmount
                    });
                }
            }
            unchecked { ++i; }
        }
        if (sellCount == 0) revert RouterInputs.EmptyArr(); // nothing overweight
    }

}"
    },
    "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/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/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/common/interface/imActive.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

abstract contract ImActive is ImDirectory {
    bool public isActive = true;

    modifier onlyActive() {
        require(isActive, "Contract is not active");
        _;
    }

    function setActive(bool _active) public onlyOwner {
        isActive = _active;
    }
}"
    },
    "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/iEtfVault.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

interface IEtfVault{
   function redeem(address tokenAddress, uint256 amount, address receiver) external;
   function initialize(address _directory) external;
}
"
    },
    "hopium/common/types/bips.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IEtfToken {
   function initialize(uint256 etfId_, string memory name_, string memory symbol_, address directory_) external;
   function mint(address to, uint256 amount) external;
   function burn(address from, uint256 amount) external;
}
"
    },
    "hopium/etf/interface/imEtfOracle.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/iEtfOracle.sol";

abstract contract ImEtfOracle is ImDirectory {

    function getEtfOracle() internal view virtual returns (IEtfOracle) {
        return IEtfOracle(fetchFromDirectory("etf-oracle"));
    }
}"
    },
    "hopium/uniswap/interface/imMultiSwapRouter.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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

interface IMultiSwapRouter {
    function swapEthToMultipleTokens(
        MultiTokenOutput[] calldata outputs,
        address recipientAddress,
        uint32 slippageBps
    ) external payable;

    function swapMultipleTokensToEth(
        MultiTokenInput[] calldata inputsIn,
        address payable recipientAddress,
        uint32 slippageBps,
        bool preTransferred
    ) external;
}

abstract contract ImMultiSwapRouter is ImDirectory {

    function getMultiSwapRouter() internal view virtual returns (IMultiSwapRouter) {
        return IMultiSwapRouter(fetchFromDirectory("multi-swap-router"));
    }
}"
    },
    "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}
"
    },
    "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/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/multiswap.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

struct MultiTokenInput {
    address tokenAddress;
    uint256 amount; // For tokens->ETH: amount of the ERC20 to sell
}

struct MultiTokenOutput {
    address tokenAddress;
    uint16  weightBips; // Weight in basis points (must sum to <= 10_000)
}"
    },
    "hopium/etf/lib/hamilton.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

library Hamilton {
    /// @dev Distribute `total` across items proportionally to `numerators`,
    /// using Hamilton/Largest Remainder. If `caps` is provided, each item
    /// is hard-capped (same unit as `total`) and leftover is re-allocated.
    /// - Deterministic tie-breaking by lower index.
    /// - Returns an array `out` s.t. sum(out) == min(total, sum(caps or INF)).
    /// @dev Distribute `total` across items proportionally to `numerators`,
    /// using Hamilton/Largest Remainder. If `caps` is provided (same length),
    /// each item is capped (same units as `total`) and leftover is re-allocated.
    /// Deterministic tie-break by lower index. Sum(out) == min(total, sum(caps or INF)).
    function distribute(
        uint256[] memory numerators,
        uint256 total,
        uint256[] memory caps
    ) internal pure returns (uint256[] memory out) {
        uint256 n = numerators.length;
        out = new uint256[](n);
        if (n == 0 || total == 0) return out;

        uint256 sumNum;
        for (uint256 i = 0; i < n; ) { sumNum += numerators[i]; unchecked { ++i; } }
        if (sumNum == 0) return out;

        bool useCaps = (caps.length == n);

        // Floor allocation + remainder capture
        uint256 acc;
        uint256[] memory rem = new uint256[](n); // in [0, sumNum)
        for (uint256 i = 0; i < n; ) {
            uint256 num = numerators[i];
            if (num == 0) { unchecked { ++i; } continue; }

            uint256 prod = num * total;
            uint256 w    = prod / sumNum;         // floor
            uint256 r    = prod - w * sumNum;     // remainder

            if (useCaps && w > caps[i]) { // respect caps on the floor pass
                w = caps[i];
                r = 0;                    // saturated → don't compete for remainder
            }

            out[i] = w;
            rem[i] = r;
            acc += w;
            unchecked { ++i; }
        }

        if (acc == total) return out;

        if (acc > total) {
            // Overshoot can happen if caps chopped floors. Trim smallest remainders first.
            uint256 over = acc - total;
            while (over != 0) {
                uint256 bestIdx = type(uint256).max;
                uint256 bestRem = type(uint256).max;
                for (uint256 i = 0; i < n; ) {
                    if (out[i] != 0 && rem[i] < bestRem) { bestRem = rem[i]; bestIdx = i; }
                    unchecked { ++i; }
                }
                if (bestIdx == type(uint256).max) break;
                out[bestIdx] -= 1;
                rem[bestIdx] = type(uint256).max; // avoid trimming same index repeatedly
                unchecked { --over; }
            }
            return out;
        }

        // acc < total → hand out remaining units to largest remainders, honoring caps
        uint256 need = total - acc;
        while (need != 0) {
            uint256 bestIdx = type(uint256).max;
            uint256 bestRem = 0;

            for (uint256 i = 0; i < n; ) {
                if (!useCaps || out[i] < caps[i]) {
                    if (rem[i] > bestRem) { bestRem = rem[i]; bestIdx = i; }
                }
                unchecked { ++i; }
            }

            if (bestIdx == type(uint256).max || bestRem == 0) {
                // No fractional winners or all capped → greedily fill first eligible indices
                for (uint256 i = 0; i < n && need != 0; ) {
                    if (!useCaps || out[i] < caps[i]) { out[i] += 1; unchecked { --need; } }
                    unchecked { ++i; }
                }
                break;
            } else {
                out[bestIdx] += 1;
                rem[bestIdx] = 0; // give others a turn on the next iteration
                unchecked { --need; }
            }
        }
        return out;
    }
}"
    },
    "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/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/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/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, Mintable, Burnable, Swap, Factory, Oracle|addr:0xbf053723de84bc53d75f22e95791194e6601aff5|verified:true|block:23668333|tx:0x922df34db4f6e68dd2dbc71bfcd7d99f73574506285fb01c5f791a87da2ad8c6|first_check:1761567305

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

Comments

Log in to comment.

No comments yet.