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": []
}
}}
Submitted on: 2025-10-27 13:15:06
Comments
Log in to comment.
No comments yet.