Address
0x26e1c36f8fcd4ffd6cbf047e91be8e5e8829f61eCurrent Holdings
$0.00
TXs sent
not counted
First Active
2026-07-22
block 27,092,566
Last Active
46 days ago
block 27,200,138
Funded By
not identified
Net worth historyi
2 snapshots · to block 27,480,474coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchStrategyVaultsolc 0.8.29+commit.ab55807cruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;
// OpenZeppelin v5 (Remix resolves these automatically)
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title StrategyVault
* @notice An immutable, ownerless vault token backed by a single ERC20 asset.
* Deployable on any EVM chain against any-decimals asset:
* the vault token automatically mirrors the asset's decimals,which
* keeps every ratio in the contract scale-free.
*
* TO CREATE A NEW VAULT, EDIT ONLY THE CONFIG BLOCK BELOW,
* then compile and deploy. Nothing else changes.
*
* Mechanism:
* - mint(): deposit asset,receive vault tokens at backing +4.5%.
* 4% stays in the vault; 0.5% to Protocol fee.
* Subject to epoch quota.
* - redeem(): burn vault tokens for pro-rata assets minus 0.5%
* held in vault. NEVER gated.
*
* EPOCH MINT QUOTA: each 7-day epoch caps minting at 10% of the
* supply existing at the epoch's first mint. Genesis epochs
* (supply == 0) are uncapped for bootstrap. Deterministic: no
* oracle, no randomness, no admin.
*
* INVARIANT: backing-per-token measured in the asset never
* decreases from any operation of this contract. Mints are
* accretive by construction (net 4.0% premium retained),
* redemptions leave the fee behind, transfers touch neither,
* donations (direct asset transfers) only add.
*
*
* No owner, no admin, no upgradability, no transfer tax.
* 100% of supply is minted publicly through
* one formula, forever.
*/
contract StrategyVault is ERC20, ReentrancyGuard {
using SafeERC20 for IERC20;
// ====================================================================
// ======================= CONFIG — EDIT ME ==========================
// ====================================================================
string private constant TOKEN_NAME = "HEXStrategy-4000";
string private constant TOKEN_SYMBOL = "HEXstr-4000";
/// The backing asset. VERIFY on the official source for the target
/// chain — immutable forever after deployment.
address private constant ASSET = 0x3Cf372aA6aAa46eDc4B8da86294deC0DDecED632; // <-- REQUIRED
/// Protocol fee recipient (0.5 of the 4.5 mint-premium points).
address public constant PROTOCOL_FEE = 0x3E5a5764EBd24d8142638366d4c5674D86c2EC64;
// ====================================================================
error ZeroAmount();
error ZeroAddress();
error ZeroTokensOut();
error SlippageExceeded();
error InsufficientBalance();
error MintQuotaExceeded(uint256 remainingQuota);
event Minted(address indexed minter, uint256 assetIn, uint256 tokensOut, uint256 newBackingPerToken);
event Redeemed(address indexed redeemer, uint256 tokensBurned, uint256 assetOut, uint256 assetFeeRetained, uint256 newBackingPerToken);
event EpochRolled(uint256 indexed epoch, uint256 quota);
event ProtocolFeePaid(address indexed recipient, uint256 amount);
IERC20 public constant asset = IERC20(ASSET);
// Economic constants — the entire design surface
uint256 public constant MINT_PREMIUM_BPS = 450; // 4.5% over backing (user pays this)
uint256 public constant PROTOCOL_FEE_BPS = 50; // 0.5 of the 4.5 points -> protocol fee; 4.0 stay in the vault
uint256 public constant REDEEM_FEE_BPS = 50; // 0.5% of redemption, retained by vault
uint256 public constant EPOCH_LENGTH = 7 days;
uint256 public constant EPOCH_QUOTA_BPS = 1_000; // 10% of supply mintable per epoch
uint256 private constant BPS = 10_000;
uint256 private constant WAD = 1e18;
/// @notice Vault token mirrors the asset's decimals (read once at deploy).
uint8 private immutable _dec;
/// @notice Total assets ever routed to the protocol fee.
uint256 public totalProtocolFees;
// Epoch state
uint256 public lastEpoch;
uint256 public epochQuota; // vault-token units mintable this epoch
uint256 public mintedThisEpoch; // vault-token units minted so far this epoch
constructor() ERC20(TOKEN_NAME, TOKEN_SYMBOL) {
if (ASSET == address(0)) revert ZeroAddress();
_dec = IERC20Metadata(ASSET).decimals();
}
/// @notice Matches the backing asset
function decimals() public view override returns (uint8) {
return _dec;
}
// --------------------------------------
// Views
// --------------------------------------
function vaultBalance() public view returns (uint256) {
return asset.balanceOf(address(this));
}
/// @notice Asset backing per token.
function backingPerToken() public view returns (uint256) {
uint256 supply = totalSupply();
if (supply == 0) return WAD;
return (vaultBalance() * WAD) / supply;
}
function currentEpoch() public view returns (uint256) {
return block.timestamp / EPOCH_LENGTH;
}
/// @notice Vault-token units still mintable right now (accounts for pending rollover).
function mintQuotaRemaining() public view returns (uint256) {
uint256 supply = totalSupply();
if (currentEpoch() != lastEpoch) {
return supply == 0 ? type(uint256).max : (supply * EPOCH_QUOTA_BPS) / BPS;
}
return mintedThisEpoch >= epochQuota ? 0 : epochQuota - mintedThisEpoch;
}
/// @notice Seconds until the next epoch (and fresh quota) begins.
function timeToNextEpoch() external view returns (uint256) {
return (currentEpoch() + 1) * EPOCH_LENGTH - block.timestamp;
}
/// @notice Vault tokens received for `assetIn` at current state (ignores quota).
function previewMint(uint256 assetIn) public view returns (uint256 tokensOut) {
uint256 supply = totalSupply();
if (supply == 0) {
tokensOut = (assetIn * BPS) / (BPS + MINT_PREMIUM_BPS);
} else {
tokensOut = (assetIn * supply * BPS) / (vaultBalance() * (BPS + MINT_PREMIUM_BPS));
}
}
/// @notice Assets received for redeeming `tokenAmount` at current state.
function previewRedeem(uint256 tokenAmount) public view returns (uint256 assetOut, uint256 assetFee) {
uint256 supply = totalSupply();
if (supply == 0) return (0, 0);
uint256 gross = (vaultBalance() * tokenAmount) / supply;
assetFee = tokenAmount == supply ? 0 : (gross * REDEEM_FEE_BPS) / BPS;
assetOut = gross - assetFee;
}
// --------------------------------------
// Mint — ceiling / premium capture, epoch-gated
// --------------------------------------
/**
* @notice Deposit the asset, receive vault tokens at backing + 4.5%
* (4.0% to the vault, 0.5% to protocol fee). Reverts when the
* epoch quota is exhausted. Requires prior asset approval.
* @param assetIn Asset amount to deposit (in the asset's decimals).
* @param minTokensOut Slippage guard (backing can shift before inclusion).
*/
function mint(uint256 assetIn, uint256 minTokensOut) external nonReentrant returns (uint256 tokensOut) {
if (assetIn == 0) revert ZeroAmount();
_rollEpochIfNeeded();
// Price on what ACTUALLY arrived (balance delta) — robust to any
// transfer quirks in the underlying asset.
uint256 vaultBefore = vaultBalance();
uint256 supply = totalSupply();
asset.safeTransferFrom(msg.sender, address(this), assetIn);
uint256 received = vaultBalance() - vaultBefore;
if (received == 0) revert ZeroAmount();
if (supply == 0) {
tokensOut = (received * BPS) / (BPS + MINT_PREMIUM_BPS);
} else {
tokensOut = (received * supply * BPS) / (vaultBefore * (BPS + MINT_PREMIUM_BPS));
}
if (tokensOut == 0) revert ZeroTokensOut();
if (tokensOut < minTokensOut) revert SlippageExceeded();
uint256 newMinted = mintedThisEpoch + tokensOut;
if (newMinted > epochQuota) {
revert MintQuotaExceeded(epochQuota > mintedThisEpoch ? epochQuota - mintedThisEpoch : 0);
}
mintedThisEpoch = newMinted;
_mint(msg.sender, tokensOut);
// Protocol fee: 0.5 of the 4.5 premium points, taken from what was
// already paid — the minter's price is unchanged, and the vault
// retains a net 4.0% premium, so every mint stays accretive.
uint256 proCut = (received * PROTOCOL_FEE_BPS) / (BPS + MINT_PREMIUM_BPS);
if (proCut > 0) {
totalProtocolFees += proCut;
asset.safeTransfer(PROTOCOL_FEE, proCut);
emit ProtocolFeePaid(PROTOCOL_FEE, proCut);
}
emit Minted(msg.sender, received, tokensOut, backingPerToken());
}
function _rollEpochIfNeeded() private {
uint256 epoch = currentEpoch();
if (epoch == lastEpoch) return;
lastEpoch = epoch;
mintedThisEpoch = 0;
uint256 supply = totalSupply();
epochQuota = supply == 0 ? type(uint256).max : (supply * EPOCH_QUOTA_BPS) / BPS;
emit EpochRolled(epoch, epochQuota);
}
// --------------------------------------
// Redeem — the floor. Never gated, never paused.
// --------------------------------------
/**
* @notice Burn vault tokens, receive pro-rata assets minus 0.5% (fee
* stays; waived when redeeming 100% of outstanding supply).
* @param minAssetOut Slippage guard.
*/
function redeem(uint256 tokenAmount, uint256 minAssetOut) external nonReentrant returns (uint256 assetOut) {
if (tokenAmount == 0) revert ZeroAmount();
if (balanceOf(msg.sender) < tokenAmount) revert InsufficientBalance();
uint256 supply = totalSupply();
uint256 gross = (vaultBalance() * tokenAmount) / supply;
// LAST-REDEEMER WAIVER: redeeming 100% of outstanding supply pays no
// fee, so no assets are ever stranded in a fully-redeemed vault.
uint256 fee = tokenAmount == supply ? 0 : (gross * REDEEM_FEE_BPS) / BPS;
assetOut = gross - fee;
if (assetOut < minAssetOut) revert SlippageExceeded();
_burn(msg.sender, tokenAmount);
asset.safeTransfer(msg.sender, assetOut);
emit Redeemed(msg.sender, tokenAmount, assetOut, fee, backingPerToken());
}
}