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