// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /* HELIX — hook-native perp DEX on Ethereum L1 * * Single-contract scaffold covering the full protocol surface: * - perp engine: open / close / liquidate, cross-margined USDC * - funding settlement: hourly, driven by (mark - oracle) skew * - HLP vault: community LP, earns 50% of fees, backstops liquidations * - $HELIX staking + buyback&burn * - 2-source oracle (chainlink + AMM TWAP median) * - admin renounced, no proxy, no upgrade path * * All financial math is in WAD (1e18) unless tagged otherwise. * Funding rates are stored as int256 in pp1e8 (1 = 0.000001%). * * This is a scaffold — gas optimization, reentrancy guards, ERC20 imports, * and the AMM hook plumbing are stubbed with TODO. Audit-targetable after fill. */ /* ----- minimal external interfaces ---------------------------------- */ interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); } interface IChainlinkAggregator { function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ); function decimals() external view returns (uint8); } /// minimal AMM-hook view interface; full Uniswap v4 hook plumbing TODO interface IAmmHook { function priceOf(bytes32 marketId) external view returns (uint256 priceWad); function twapOf(bytes32 marketId, uint32 window) external view returns (uint256 priceWad); } /* ----- HELIX core ---------------------------------------------------- */ contract HelixPerps { /* ====== constants ============================================== */ uint256 public constant WAD = 1e18; uint256 public constant BPS = 10_000; // fees, in bps uint16 public constant MAKER_FEE_BPS = 25; // 0.025% uint16 public constant TAKER_FEE_BPS = 45; // 0.045% uint16 public constant LIQ_BOUNTY_BPS = 100; // 1.00% // protocol fee split (must sum to BPS = 10_000) uint16 public constant FEE_TO_INSURANCE_BPS = 200; // 2% uint16 public constant FEE_TO_STAKERS_BPS = 4900; // 49% uint16 public constant FEE_TO_BURN_BPS = 4900; // 49% // Insurance fund — first buffer for residual liquidation loss. // Capped at $1M USDC (WAD-scaled). Overflow routes to HLP. uint256 public constant INSURANCE_FUND_CAP = 1_000_000 * WAD; // funding clamp · per hour, in pp1e8 (5e3 = 0.05%) int256 public constant FUNDING_CLAMP_PP1E8 = 5_000; // risk: cross-margin uint16 public constant INITIAL_MARGIN_BPS_MAJOR = 400; // 4% → 25× max uint16 public constant MAINT_MARGIN_BPS_MAJOR = 200; // 2% → MMR 100% trigger uint16 public constant INITIAL_MARGIN_BPS_ALT = 1000; // 10% → 10× max uint16 public constant MAINT_MARGIN_BPS_ALT = 500; // HLP safety uint16 public constant HLP_DRAWDOWN_GATE_BPS = 800; // 8% /* ====== types ================================================== */ enum Side { LONG, SHORT } struct Market { bytes32 id; // keccak256("ETH-PERP") etc. bytes32 symbol; // ascii symbol packed into bytes32 (e.g. "ETH-PERP") address oracle; // chainlink aggregator uint128 oiLong; // open interest, USD notional WAD uint128 oiShort; int128 cumFunding; // cumulative funding · pp1e8 · WAD scaled uint64 lastFundingTs; uint32 fundingWindowSec; // typically 3600 (hourly) bool isMajor; // affects margin params bool enabled; } struct Position { bytes32 marketId; Side side; uint128 margin; // USDC, 6-decimal · scaled-up to WAD on read uint128 notional; // USD WAD uint256 entryPrice; // WAD int256 fundingSnapshot; // pp1e8 · WAD scaled, at open or last realize uint64 openedAt; bool open; } struct StakeInfo { uint128 amount; uint128 rewardDebt; // for fee-share accounting (per-share pattern) } /* ====== storage ================================================ */ IERC20 public immutable USDC; IERC20 public immutable HELIX_TOKEN; IAmmHook public immutable AMM; /// the v4 hook contract authorized to call settleFunding / sweepLiquidations / /// recordMark / donateHLP. Set in constructor, immutable forever. address public immutable HOOK; // markets mapping(bytes32 => Market) public markets; bytes32[] public marketList; // positions: trader → marketId → Position // (one position per market per trader; reopen merges) mapping(address => mapping(bytes32 => Position)) public positions; // cross-margin account mapping(address => uint256) public marginBalance; // USDC, WAD scaled // HLP vault uint256 public hlpAssets; // USDC (WAD) uint256 public hlpShares; // shares outstanding mapping(address => uint256) public hlpShareBalance; uint256 public hlpPeakAssets; // for drawdown gate /* ====== bootstrap window ======================================= */ // Time-limited 2× $HELIX emissions for the first $BOOTSTRAP_CAP_USDC of HLP deposits. // Once filled OR the timer ends (whichever first), bootstrap inactive and emissions // revert to baseEmissionsPerUsdc. Vests linearly over BOOTSTRAP_VEST_DAYS. uint256 public constant BOOTSTRAP_CAP_USDC = 1_000_000e6; // 1M USDC (6 dec) uint256 public constant BOOTSTRAP_DURATION = 28 days; uint16 public constant BOOTSTRAP_MULTIPLIER_BPS = 20_000; // 2.00× = 20000 bps uint32 public constant BOOTSTRAP_VEST_DAYS = 28; uint16 public constant REFERRAL_KICKBACK_BPS = 1000; // 10% of bonus to referrer uint256 public bootstrapFilledUsdc; // 6-dec running total of bootstrap-eligible deposits uint256 public bootstrapEndTs; // unix seconds — set at deploy uint256 public baseEmissionsPerUsdc; // WAD-scaled $HELIX per 1 USDC deposited (1.6e18 default) uint256 public earlyDepositorCount; struct VestingClaim { uint128 totalHelix; // total $HELIX owed at vest end uint64 startTs; uint64 endTs; uint128 claimed; } mapping(address => VestingClaim[]) public bootstrapVests; uint256 public bootstrapEmissionsBudget; // remaining $HELIX from ecosystem allocation // Founder seed lock — founder deposits founderSeedUsdc and the corresponding // HLP shares are locked until founderLockEnd. Unlocking is gated, not slashable. address public immutable founder; uint256 public founderSeedShares; uint256 public founderLockEnd; event BootstrapDeposit(address indexed depositor, uint256 usdcAmount, uint256 helixEmitted, address referrer, uint256 referrerKickback); event BootstrapClaim(address indexed depositor, uint256 index, uint256 amount); event FounderSeed(address indexed founder, uint256 usdcAmount, uint256 shares, uint256 lockEnd); // staking · fee share accumulator (per-share) uint256 public stakedTotal; uint256 public accFeesPerShare; // cumulative WAD per token-staked mapping(address => StakeInfo) public stakes; // buyback&burn queue uint256 public buybackQueueUSDC; uint256 public totalBurned; // insurance fund (WAD-scaled USDC) — first buffer before HLP absorbs uint256 public insuranceFundUsdc; // markets registry locked after deploy init bool public marketsLocked; address public immutable INIT_ADMIN; // only this address can call addMarket/lockMarkets, until locked // reentrancy guard uint256 private _reentrancyLock = 1; // 1 = unlocked, 2 = locked /* ====== events ================================================= */ event MarketsLocked(); event InsuranceFunded(uint256 amount, uint256 newBalance); event InsuranceDrained(uint256 amount, uint256 newBalance); event MarketAdded(bytes32 indexed marketId, bytes32 symbol, address oracle, bool isMajor, uint32 fundingWindowSec); event Deposit(address indexed trader, uint256 amount); event Withdraw(address indexed trader, uint256 amount); event Open( address indexed trader, bytes32 indexed marketId, Side side, uint256 margin, uint256 notional, uint256 entryPrice, uint256 fee ); event Close( address indexed trader, bytes32 indexed marketId, uint256 notional, uint256 exitPrice, int256 pnl, uint256 fee ); event Liquidate( address indexed trader, bytes32 indexed marketId, address indexed liquidator, uint256 seized, uint256 bounty, int256 hlpDelta ); event FundingSettled(bytes32 indexed marketId, int256 newCum, int256 rate); event HLPDeposit(address indexed depositor, uint256 assets, uint256 shares); event HLPWithdraw(address indexed depositor, uint256 assets, uint256 shares); event Stake(address indexed staker, uint256 amount); event Unstake(address indexed staker, uint256 amount); event ClaimFees(address indexed staker, uint256 amount); event Buyback(uint256 usdcIn, uint256 helixOut); event Burn(uint256 amount); /* ====== constructor — admin renounced at deploy =============== */ constructor( IERC20 _usdc, IERC20 _helix, IAmmHook _amm, address _hook, address _founder, uint256 _bootstrapEmissionsBudget, uint256 _baseEmissionsPerUsdc ) { USDC = _usdc; HELIX_TOKEN = _helix; AMM = _amm; HOOK = _hook; // INIT_ADMIN can call addMarket + lockMarkets until lockMarkets() is invoked, // after which the registry is frozen forever. There is no other admin role. INIT_ADMIN = msg.sender; // bootstrap window — starts at deploy, runs for BOOTSTRAP_DURATION. bootstrapEndTs = block.timestamp + BOOTSTRAP_DURATION; baseEmissionsPerUsdc = _baseEmissionsPerUsdc; // e.g. 1.6e18 bootstrapEmissionsBudget = _bootstrapEmissionsBudget; // pre-funded from ecosystem alloc founder = _founder; founderLockEnd = block.timestamp + 90 days; } /* ====== reentrancy guard ====================================== */ modifier nonReentrant() { require(_reentrancyLock == 1, "reentrant"); _reentrancyLock = 2; _; _reentrancyLock = 1; } /* ====== safe ERC20 ============================================ */ // USDC and a vanilla $HELIX both revert on failure, but defending here against // any future token that returns false instead of reverting (per slither). function _safeTransfer(IERC20 token, address to, uint256 amount) internal { (bool s, bytes memory data) = address(token).call( abi.encodeWithSelector(IERC20.transfer.selector, to, amount) ); require(s && (data.length == 0 || abi.decode(data, (bool))), "transfer failed"); } function _safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal { (bool s, bytes memory data) = address(token).call( abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount) ); require(s && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed"); } /// only the v4 hook contract may call functions tagged onlyHook. /// the hook itself is immutable and has no admin — so this is a one-shot bind. modifier onlyHook() { require(msg.sender == HOOK, "only hook"); _; } /* ====== markets (one-shot at deploy via init batch) ============ */ /// add a market. callable only by INIT_ADMIN (deployer) until `lockMarkets()` is called, /// after which the registry is frozen forever. `symbol` is the ascii market name /// packed into bytes32 (e.g. "ETH-PERP"); used by indexers and the chain reader. function addMarket(bytes32 marketId, bytes32 symbol, address oracle, bool isMajor, uint32 fundingWindowSec) external { require(!marketsLocked, "markets locked"); require(msg.sender == INIT_ADMIN, "only init admin"); require(markets[marketId].id == bytes32(0), "exists"); require(marketId == keccak256(abi.encodePacked(symbol)), "sym/id mismatch"); markets[marketId] = Market({ id: marketId, symbol: symbol, oracle: oracle, oiLong: 0, oiShort: 0, cumFunding: 0, lastFundingTs: uint64(block.timestamp), fundingWindowSec: fundingWindowSec, isMajor: isMajor, enabled: true }); marketList.push(marketId); emit MarketAdded(marketId, symbol, oracle, isMajor, fundingWindowSec); } /* ====== cross-margin account ================================= */ /// One-shot finalization of the markets registry. After this, no further /// addMarket calls are accepted — registry is frozen forever. Idempotent revert. function lockMarkets() external { require(!marketsLocked, "already locked"); require(msg.sender == INIT_ADMIN, "only init admin"); marketsLocked = true; emit MarketsLocked(); } function deposit(uint256 amount) external nonReentrant { _safeTransferFrom(USDC, msg.sender, address(this), amount); marginBalance[msg.sender] += amount * (WAD / 1e6); // 6 → 18 dec emit Deposit(msg.sender, amount); } function withdraw(uint256 amount) external nonReentrant { // check no open positions that would breach margin require(_freeMargin(msg.sender) >= amount, "margin"); marginBalance[msg.sender] -= amount; _safeTransfer(USDC, msg.sender, amount / (WAD / 1e6)); emit Withdraw(msg.sender, amount); } /* ====== open / close ========================================== */ /// open or add to a position. notional in USD WAD, lev decoded from notional/margin. function open(bytes32 marketId, Side side, uint256 marginAdd, uint256 notional) external nonReentrant { Market storage m = markets[marketId]; require(m.enabled, "mkt"); // settle funding for market _settleFunding(marketId); // require margin meets initial margin requirement uint16 imBps = m.isMajor ? INITIAL_MARGIN_BPS_MAJOR : INITIAL_MARGIN_BPS_ALT; require(marginAdd * BPS >= notional * imBps / WAD, "init margin"); // pull margin into account if needed if (marginAdd > 0) { require(marginBalance[msg.sender] >= marginAdd, "bal"); // margin is fungible in cross-margin account; we just track notional } // fee: 0.045% taker uint256 fee = notional * TAKER_FEE_BPS / BPS; require(marginBalance[msg.sender] >= fee, "fee"); marginBalance[msg.sender] -= fee; _distributeFees(fee); // entry price from AMM hook uint256 entry = AMM.priceOf(marketId); Position storage p = positions[msg.sender][marketId]; if (!p.open) { p.marketId = marketId; p.side = side; p.margin = uint128(marginAdd); p.notional = uint128(notional); p.entryPrice = entry; p.fundingSnapshot = m.cumFunding; p.openedAt = uint64(block.timestamp); p.open = true; } else { require(p.side == side, "same side"); // weighted-avg entry uint256 newNotional = uint256(p.notional) + notional; p.entryPrice = (uint256(p.notional) * p.entryPrice + notional * entry) / newNotional; p.notional = uint128(newNotional); p.margin = uint128(uint256(p.margin) + marginAdd); } if (side == Side.LONG) m.oiLong += uint128(notional); else m.oiShort += uint128(notional); emit Open(msg.sender, marketId, side, marginAdd, notional, entry, fee); } function close(bytes32 marketId, uint256 notional) external nonReentrant { Position storage p = positions[msg.sender][marketId]; require(p.open, "no pos"); require(notional <= p.notional, "size"); Market storage m = markets[marketId]; _settleFunding(marketId); uint256 exitPrice = AMM.priceOf(marketId); int256 pnl = _calcPnL(p, exitPrice, notional); // funding owed since last snapshot int256 fundingDelta = m.cumFunding - p.fundingSnapshot; int256 fundingPnL = (int256(uint256(p.notional)) * fundingDelta) / int256(WAD); if (p.side == Side.SHORT) fundingPnL = -fundingPnL; pnl -= fundingPnL; uint256 fee = notional * MAKER_FEE_BPS / BPS; // apply pnl + fee if (pnl >= 0) { marginBalance[msg.sender] += uint256(pnl); } else { uint256 loss = uint256(-pnl); require(marginBalance[msg.sender] >= loss + fee, "blown"); marginBalance[msg.sender] -= loss; } require(marginBalance[msg.sender] >= fee, "fee"); marginBalance[msg.sender] -= fee; _distributeFees(fee); // unwind OI if (p.side == Side.LONG) m.oiLong -= uint128(notional); else m.oiShort -= uint128(notional); p.notional = uint128(uint256(p.notional) - notional); p.fundingSnapshot = m.cumFunding; if (p.notional == 0) p.open = false; emit Close(msg.sender, marketId, notional, exitPrice, pnl, fee); } /* ====== liquidation ========================================== */ /// External entry: anyone can liquidate an unhealthy position and collect /// the 1% bounty. Thin wrapper around _liquidateInternal — msg.sender is /// the bounty recipient. function liquidate(address trader, bytes32 marketId) external nonReentrant { _liquidateInternal(trader, marketId, msg.sender); } /// Canonical liquidation logic. Used by: /// - external `liquidate()` — caller is the bounty recipient /// - hook-driven `sweepLiquidations()` — bountyTo = address(0), in /// which case the bounty accrues to HLP instead of an EOA. /// Reverts if the position is healthy or doesn't exist. function _liquidateInternal(address trader, bytes32 marketId, address bountyTo) internal { Position storage p = positions[trader][marketId]; require(p.open, "no pos"); Market storage m = markets[marketId]; _settleFunding(marketId); // prefer the trade-cleared mark when fresh; otherwise fall back to pool spot uint256 mark = lastClearedMark[marketId]; if (mark == 0) mark = AMM.priceOf(marketId); // MMR check: equity / notional < maintenance margin int256 equity = _equity(p, mark, m.cumFunding); uint16 mmBps = m.isMajor ? MAINT_MARGIN_BPS_MAJOR : MAINT_MARGIN_BPS_ALT; require(equity * int256(BPS) < int256(uint256(p.notional)) * int256(uint256(mmBps)) / int256(WAD), "healthy"); // seize: equity goes to bounty recipient, residual to trader (if equity > bounty) // or to HLP (if equity is negative — vault absorbs the shortfall) uint256 bounty = uint256(p.notional) * LIQ_BOUNTY_BPS / BPS; if (equity > int256(bounty)) { // healthy seize: residual back to trader, full bounty out uint256 residual = uint256(equity) - bounty; marginBalance[trader] += residual; _routeBounty(bountyTo, bounty); emit Liquidate(trader, marketId, _bountyAddr(bountyTo), uint256(equity), bounty, int256(0)); } else if (equity > 0) { // partial bounty — whole equity goes to bounty recipient uint256 bountyOut = uint256(equity); _routeBounty(bountyTo, bountyOut); emit Liquidate(trader, marketId, _bountyAddr(bountyTo), uint256(equity), bountyOut, int256(0)); } else { // residual loss · insurance fund first, HLP absorbs remainder uint256 shortfall = uint256(-equity); uint256 fromInsurance = shortfall > insuranceFundUsdc ? insuranceFundUsdc : shortfall; if (fromInsurance > 0) { insuranceFundUsdc -= fromInsurance; emit InsuranceDrained(fromInsurance, insuranceFundUsdc); } uint256 remaining = shortfall - fromInsurance; if (remaining > 0) { require(hlpAssets >= remaining, "HLP drained"); hlpAssets -= remaining; } emit Liquidate(trader, marketId, _bountyAddr(bountyTo), 0, 0, -int256(shortfall)); } // unwind OI if (p.side == Side.LONG) m.oiLong -= p.notional; else m.oiShort -= p.notional; p.notional = 0; p.margin = 0; p.open = false; } /// route bounty to an EOA (external liquidate) or credit HLP (queue sweep) function _routeBounty(address bountyTo, uint256 amountWad) internal { if (bountyTo == address(0)) { // queue-driven sweep — bounty stays in protocol, credited to HLP hlpAssets += amountWad; if (hlpAssets > hlpPeakAssets) hlpPeakAssets = hlpAssets; } else { // external liquidator — pay out in USDC (6 dec) _safeTransfer(USDC, bountyTo, amountWad / (WAD / 1e6)); } } /// canonical bounty address for the Liquidate event — uses HOOK as the /// "liquidator" attribution when the sweep path was taken function _bountyAddr(address bountyTo) internal view returns (address) { return bountyTo == address(0) ? HOOK : bountyTo; } /* ====== funding =============================================== */ /// settle funding for a market. callable by anyone. function _settleFunding(bytes32 marketId) internal { Market storage m = markets[marketId]; uint256 elapsed = block.timestamp - m.lastFundingTs; if (elapsed < m.fundingWindowSec) return; uint256 mark = AMM.priceOf(marketId); uint256 oracle = _oracleMedian(marketId); // rate per hour, pp1e8 — clamp int256 basis; if (mark >= oracle) basis = int256((mark - oracle) * 1e8 / oracle); else basis = -int256((oracle - mark) * 1e8 / oracle); int256 rate = basis; // 1× scaling; production may use k≠1 if (rate > FUNDING_CLAMP_PP1E8) rate = FUNDING_CLAMP_PP1E8; if (rate < -FUNDING_CLAMP_PP1E8) rate = -FUNDING_CLAMP_PP1E8; // accumulate · scaled by elapsed hours · WAD int256 hours_ = int256(elapsed / m.fundingWindowSec); int256 cumDelta = rate * hours_ * int256(WAD) / 1e8; m.cumFunding = int128(int256(m.cumFunding) + cumDelta); m.lastFundingTs = uint64(m.lastFundingTs + uint256(hours_) * m.fundingWindowSec); emit FundingSettled(marketId, m.cumFunding, rate); } function _oracleMedian(bytes32 marketId) internal view returns (uint256) { Market storage m = markets[marketId]; // 2-source: chainlink + AMM TWAP (median) (, int256 cl,,,) = IChainlinkAggregator(m.oracle).latestRoundData(); uint8 dec = IChainlinkAggregator(m.oracle).decimals(); uint256 chainlink = uint256(cl) * (10 ** (18 - dec)); uint256 twap = AMM.twapOf(marketId, 600); // 10min twap return chainlink < twap ? (chainlink + twap) / 2 : (twap + chainlink) / 2; } /* ====== HLP vault ============================================= */ function hlpDeposit(uint256 assets) external nonReentrant { _hlpDepositInternal(msg.sender, assets, address(0)); } /// Deposit with referral. `referrer` collects REFERRAL_KICKBACK_BPS of the bonus emissions. function hlpDepositWithReferral(uint256 assets, address referrer) external nonReentrant { _hlpDepositInternal(msg.sender, assets, referrer); } function _hlpDepositInternal(address depositor, uint256 assets, address referrer) internal { _safeTransferFrom(USDC, depositor, address(this), assets); uint256 assetsWad = assets * (WAD / 1e6); uint256 shares = hlpShares == 0 ? assetsWad : assetsWad * hlpShares / hlpAssets; hlpAssets += assetsWad; hlpShares += shares; hlpShareBalance[depositor] += shares; if (hlpAssets > hlpPeakAssets) hlpPeakAssets = hlpAssets; // ----- bootstrap accounting ----- uint256 helixEmitted = 0; uint256 referrerKickback = 0; bool active = (block.timestamp < bootstrapEndTs) && (bootstrapFilledUsdc < BOOTSTRAP_CAP_USDC); if (active) { uint256 bootstrapApplied = assets; uint256 remainingCap = BOOTSTRAP_CAP_USDC - bootstrapFilledUsdc; if (bootstrapApplied > remainingCap) bootstrapApplied = remainingCap; bootstrapFilledUsdc += bootstrapApplied; // emissions = applied · base · multiplier ; remainder gets base only uint256 bonusHelix = (bootstrapApplied * baseEmissionsPerUsdc * BOOTSTRAP_MULTIPLIER_BPS) / (1e6 * BPS); uint256 baseRemainderHelix = ((assets - bootstrapApplied) * baseEmissionsPerUsdc) / 1e6; helixEmitted = bonusHelix + baseRemainderHelix; if (bootstrapApplied >= 100 * 1e6) earlyDepositorCount += 1; if (referrer != address(0) && referrer != depositor) { referrerKickback = (bonusHelix * REFERRAL_KICKBACK_BPS) / BPS; } } else { helixEmitted = (assets * baseEmissionsPerUsdc) / 1e6; } // schedule vesting (28d linear) if (helixEmitted > 0) { require(bootstrapEmissionsBudget >= helixEmitted + referrerKickback, "emissions exhausted"); bootstrapEmissionsBudget -= (helixEmitted + referrerKickback); bootstrapVests[depositor].push(VestingClaim({ totalHelix: uint128(helixEmitted), startTs: uint64(block.timestamp), endTs: uint64(block.timestamp + uint256(BOOTSTRAP_VEST_DAYS) * 1 days), claimed: 0 })); if (referrerKickback > 0) { bootstrapVests[referrer].push(VestingClaim({ totalHelix: uint128(referrerKickback), startTs: uint64(block.timestamp), endTs: uint64(block.timestamp + uint256(BOOTSTRAP_VEST_DAYS) * 1 days), claimed: 0 })); } } emit HLPDeposit(depositor, assets, shares); if (active && helixEmitted > 0) { emit BootstrapDeposit(depositor, assets, helixEmitted, referrer, referrerKickback); } } /// Claim vested $HELIX from bootstrap deposits. Iterates the depositor's vest schedule /// and pays out everything matured up to `block.timestamp`. Anyone can call for themselves. function bootstrapClaim() external nonReentrant { VestingClaim[] storage vs = bootstrapVests[msg.sender]; uint256 total = 0; for (uint256 i = 0; i < vs.length; i++) { VestingClaim storage v = vs[i]; if (v.claimed >= v.totalHelix) continue; uint256 vested; if (block.timestamp >= v.endTs) { vested = v.totalHelix; } else { vested = (uint256(v.totalHelix) * (block.timestamp - v.startTs)) / (v.endTs - v.startTs); } uint256 owed = vested - v.claimed; if (owed > 0) { v.claimed = uint128(vested); total += owed; emit BootstrapClaim(msg.sender, i, owed); } } if (total > 0) _safeTransfer(HELIX_TOKEN, msg.sender, total); } function bootstrapClaimable(address who) external view returns (uint256 total) { VestingClaim[] storage vs = bootstrapVests[who]; for (uint256 i = 0; i < vs.length; i++) { VestingClaim storage v = vs[i]; if (v.claimed >= v.totalHelix) continue; uint256 vested = block.timestamp >= v.endTs ? v.totalHelix : (uint256(v.totalHelix) * (block.timestamp - v.startTs)) / (v.endTs - v.startTs); total += vested - v.claimed; } } /// Founder day-1 seed — single call, locks shares for 90d. /// callable only by `founder` and only once. function seedFounderDeposit(uint256 assets) external nonReentrant { require(msg.sender == founder, "only founder"); require(founderSeedShares == 0, "seeded"); _hlpDepositInternal(founder, assets, address(0)); // mark the freshest deposit's shares as locked founderSeedShares = hlpShareBalance[founder]; emit FounderSeed(founder, assets, founderSeedShares, founderLockEnd); } function hlpWithdraw(uint256 shares) external nonReentrant { require(hlpShareBalance[msg.sender] >= shares, "shares"); // drawdown gate: if hlpAssets < peak * (1 - 8%), block withdrawals require(hlpAssets * BPS >= hlpPeakAssets * (BPS - HLP_DRAWDOWN_GATE_BPS), "drawdown gate"); // founder seed shares are locked until founderLockEnd (90d post-deploy) if (msg.sender == founder && block.timestamp < founderLockEnd) { require(hlpShareBalance[founder] - shares >= founderSeedShares, "founder lock"); } uint256 assetsWad = shares * hlpAssets / hlpShares; hlpAssets -= assetsWad; hlpShares -= shares; hlpShareBalance[msg.sender] -= shares; _safeTransfer(USDC, msg.sender, assetsWad / (WAD / 1e6)); emit HLPWithdraw(msg.sender, assetsWad / (WAD / 1e6), shares); } /* ====== $HELIX staking · fee share ============================ */ function stake(uint256 amount) external nonReentrant { _harvest(msg.sender); _safeTransferFrom(HELIX_TOKEN, msg.sender, address(this), amount); stakes[msg.sender].amount += uint128(amount); stakedTotal += amount; stakes[msg.sender].rewardDebt = uint128(uint256(stakes[msg.sender].amount) * accFeesPerShare / WAD); emit Stake(msg.sender, amount); } function unstake(uint256 amount) external nonReentrant { _harvest(msg.sender); require(stakes[msg.sender].amount >= amount, "stake"); stakes[msg.sender].amount -= uint128(amount); stakedTotal -= amount; stakes[msg.sender].rewardDebt = uint128(uint256(stakes[msg.sender].amount) * accFeesPerShare / WAD); _safeTransfer(HELIX_TOKEN, msg.sender, amount); emit Unstake(msg.sender, amount); } function _harvest(address staker) internal { uint256 pending = uint256(stakes[staker].amount) * accFeesPerShare / WAD - stakes[staker].rewardDebt; if (pending > 0) { _safeTransfer(USDC, staker, pending / (WAD / 1e6)); emit ClaimFees(staker, pending); } } function claim() external nonReentrant { _harvest(msg.sender); stakes[msg.sender].rewardDebt = uint128(uint256(stakes[msg.sender].amount) * accFeesPerShare / WAD); } /* ====== fee distribution + buyback ============================ */ function _distributeFees(uint256 fee) internal { // 2% to insurance fund (until cap), overflow → HLP uint256 toInsurance = fee * FEE_TO_INSURANCE_BPS / BPS; if (toInsurance > 0) { uint256 room = insuranceFundUsdc < INSURANCE_FUND_CAP ? INSURANCE_FUND_CAP - insuranceFundUsdc : 0; uint256 absorbed = toInsurance > room ? room : toInsurance; if (absorbed > 0) { insuranceFundUsdc += absorbed; emit InsuranceFunded(absorbed, insuranceFundUsdc); } uint256 overflow = toInsurance - absorbed; if (overflow > 0) hlpAssets += overflow; } // 49% to stakers (per-share accumulator) uint256 toStakers = fee * FEE_TO_STAKERS_BPS / BPS; if (stakedTotal > 0) { accFeesPerShare += toStakers * WAD / stakedTotal; } else { // no stakers · route to HLP as bonus hlpAssets += toStakers; } // 49% to buyback queue uint256 toBuyback = fee * FEE_TO_BURN_BPS / BPS; buybackQueueUSDC += toBuyback; } /// anyone can trigger the buyback once queue exceeds threshold function executeBuyback() external nonReentrant { require(buybackQueueUSDC >= 1000 * WAD, "small"); // $1,000 min uint256 usdcIn = buybackQueueUSDC; buybackQueueUSDC = 0; // TODO: swap usdcIn USDC → HELIX via AMM hook // uint256 helixOut = AMM.swap(USDC, HELIX_TOKEN, usdcIn); uint256 helixOut = 0; // stub // send to dead address if (helixOut > 0) { _safeTransfer(HELIX_TOKEN, address(0xdead), helixOut); totalBurned += helixOut; emit Burn(helixOut); } emit Buyback(usdcIn, helixOut); } /* ====== views ================================================= */ function _calcPnL(Position storage p, uint256 exitPrice, uint256 size) internal view returns (int256) { // longPnL = (exit - entry) / entry * size // shortPnL = (entry - exit) / entry * size int256 dpx = p.side == Side.LONG ? int256(exitPrice) - int256(p.entryPrice) : int256(p.entryPrice) - int256(exitPrice); return dpx * int256(size) / int256(p.entryPrice); } function _equity(Position storage p, uint256 mark, int256 cumFunding) internal view returns (int256) { int256 pnl = _calcPnL(p, mark, p.notional); int256 fundingDelta = cumFunding - p.fundingSnapshot; int256 fundingPnL = int256(uint256(p.notional)) * fundingDelta / int256(WAD); if (p.side == Side.SHORT) fundingPnL = -fundingPnL; return int256(uint256(p.margin)) + pnl - fundingPnL; } function _freeMargin(address trader) internal view returns (uint256) { // sum reserved margin across all open positions; subtract from balance // TODO: iterate positions (gas-bounded — use list per trader in production) return marginBalance[trader]; } function marketCount() external view returns (uint256) { return marketList.length; } function previewPosition(address trader, bytes32 marketId) external view returns (Side side, uint256 margin, uint256 notional, uint256 entryPrice, int256 unrealizedPnL, int256 fundingPnL) { Position storage p = positions[trader][marketId]; Market storage m = markets[marketId]; side = p.side; margin = p.margin; notional = p.notional; entryPrice = p.entryPrice; if (p.open) { uint256 mark = AMM.priceOf(marketId); unrealizedPnL = _calcPnL(p, mark, p.notional); int256 fd = m.cumFunding - p.fundingSnapshot; fundingPnL = int256(uint256(p.notional)) * fd / int256(WAD); if (p.side == Side.SHORT) fundingPnL = -fundingPnL; } } /* ====== hook-facing entries ===================================== */ /* * The HelixHook contract (the v4 hook for each market's pool) calls * these on every swap. None of them are callable by anyone else. * * Design notes: * - settleFunding takes the mark from the hook (which read it pre-swap) * so the funding cadence is driven by trade flow, not by keepers. * - sweepLiquidations pops from a per-market keeper-fed queue; we don't * enumerate all positions on every swap (gas). * - recordMark writes the trade-cleared price after the swap; this is * what the engine treats as the canonical mark for the next block. * - donateHLP routes a tiny bps slice of every spot swap into the * vault. Composes with the perp fee share; vault yield = perp + spot. */ /// last trade-cleared mark per market, written by the hook on afterSwap. /// reads (preview, liquidate) prefer this when fresher than AMM.priceOf(). mapping(bytes32 => uint256) public lastClearedMark; mapping(bytes32 => uint64) public lastClearedTs; /// keeper-fed liquidation candidate queue. anyone can push a trader who /// they believe is liquidatable; the hook pops and verifies on each swap. /// invalid candidates are dropped without paying the bounty. mapping(bytes32 => address[]) public liqQueue; /// running total of HLP donations received from spot swap volume, per market. /// purely informational; vault credit happens in real-time in donateHLP(). mapping(bytes32 => uint256) public hlpDonatedFromSwaps; event MarkRecorded(bytes32 indexed marketId, uint256 mark, uint64 ts); event HLPDonatedFromSwap(bytes32 indexed marketId, uint256 amount, uint256 cumulative); event LiqQueued(bytes32 indexed marketId, address indexed trader); event LiqSwept(bytes32 indexed marketId, address indexed trader, bool liquidated); /// anyone can flag a candidate. Stays in queue until processed by a swap. /// the queue is best-effort — invalid entries cost only one storage slot. function flagForLiq(bytes32 marketId, address trader) external { liqQueue[marketId].push(trader); emit LiqQueued(marketId, trader); } /// hook → engine: settle funding using the pre-swap mark from the pool. function settleFunding(bytes32 marketId, uint256 /* markPriceWad */) external onlyHook { _settleFunding(marketId); } /// hook → engine: try to liquidate up to `max` queued candidates. /// returns the count actually liquidated (≤ max). Skips healthy positions. function sweepLiquidations(bytes32 marketId, uint8 max) external onlyHook returns (uint8 swept) { address[] storage q = liqQueue[marketId]; Market storage m = markets[marketId]; uint8 popped = 0; while (q.length > 0 && popped < max) { address trader = q[q.length - 1]; q.pop(); popped++; Position storage p = positions[trader][marketId]; if (!p.open) { emit LiqSwept(marketId, trader, false); continue; } uint256 mark = lastClearedMark[marketId]; if (mark == 0) mark = AMM.priceOf(marketId); int256 equity = _equity(p, mark, m.cumFunding); uint16 mmBps = m.isMajor ? MAINT_MARGIN_BPS_MAJOR : MAINT_MARGIN_BPS_ALT; bool unhealthy = equity * int256(BPS) < int256(uint256(p.notional)) * int256(uint256(mmBps)) / int256(WAD); if (!unhealthy) { emit LiqSwept(marketId, trader, false); continue; } // canonical liquidate path; bountyTo=0 routes the bounty to HLP // since there's no liquidator EOA in the queue-sweep flow _liquidateInternal(trader, marketId, address(0)); swept++; emit LiqSwept(marketId, trader, true); } } /// hook → engine: write the trade-cleared mark from afterSwap. /// This is the canonical mark for the next block's preview / equity reads. function recordMark(bytes32 marketId, uint256 markPriceWad) external onlyHook { if (markPriceWad == 0) return; lastClearedMark[marketId] = markPriceWad; lastClearedTs[marketId] = uint64(block.timestamp); emit MarkRecorded(marketId, markPriceWad, uint64(block.timestamp)); } /// hook → engine: route a slice of spot-swap value into HLP. The hook /// must have transferred the USDC to this contract before calling; we /// credit the vault and update the running total. function donateHLP(bytes32 marketId, uint256 usdcAmount) external onlyHook { if (usdcAmount == 0) return; // the hook pulls quote-side tokens from the swap router and transfers // them here in the same tx — for the scaffold we assume the balance is in. hlpAssets += usdcAmount; if (hlpAssets > hlpPeakAssets) hlpPeakAssets = hlpAssets; hlpDonatedFromSwaps[marketId] += usdcAmount; emit HLPDonatedFromSwap(marketId, usdcAmount, hlpDonatedFromSwaps[marketId]); } /* ====== safety net — receive() blocks accidental ETH =========== */ receive() external payable { revert("no ETH"); } }