Skip to main content
PulseScanner.io

Address

0x26e1c36f8fcd4ffd6cbf047e91be8e5e8829f61e
Current 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());
    }
}