XHissVault
0x699861d2c546ab86a7f2ae97ffc7af89f3ff67be
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
7,668 bytes
ABI entries
77
29 read · 15 write
License
none
Contract information
- Address
- 0x699861d2c546ab86a7f2ae97ffc7af89f3ff67be
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0x93bd29c657…1a63620609
Token
- Name
- Staked HISS
- Symbol
- xHISS
- Decimals
- 18
- Holders
- 7
- Market
- View token page →
Read contract (29)
COOLDOWN_SECONDS() → uint64
DRIP_SECONDS() → uint64
REDEEM_WINDOW_SECONDS() → uint64
ROBINHOOD_CHAIN_ID() → uint256
ROBINHOOD_CHAIN_TESTNET_ID() → uint256
allowance(address, address) → uint256
balanceOf(address) → uint256
convertToAssets(uint256) → uint256
convertToShares(uint256) → uint256
cooldownOf(address) → uint256, uint64, uint64
cooldowns(address) → uint192, uint64
decimals() → uint8
dripAmount() → uint256
dripEnd() → uint64
dripStart() → uint64
hiss() → address
hissPerXHiss() → uint256
isInjector(address) → bool
name() → string
owner() → address
paused() → bool
pendingOwner() → address
previewRedeem(uint256) → uint256
previewStake(uint256) → uint256
symbol() → string
totalHissStaked() → uint256
totalSupply() → uint256
totalXHissSupply() → uint256
unvestedRewards() → uint256
Events (13)
ApprovalCooldownCancelledCooldownRestartedCooldownStartedInjectorSetOwnershipTransferStartedOwnershipTransferredRedeemedRewardsInjectedStakedTokenRescuedTransferVaultPaused
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "hiss_",
"type": "address"
},
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "CannotRescueStakeAsset",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
}
],
"name": "CooldownNotElapsed",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientAllowance",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "requested",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "cooling",
"type": "uint256"
}
],
"name": "InsufficientCooldownShares",
"type": "error"
},
{
"inputs": [],
"name": "NoCooldown",
"type": "error"
},
{
"inputs": [],
"name": "NoStakers",
"type": "error"
},
{
"inputs": [],
"name": "NotInjector",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "NotPaused",
"type": "error"
},
{
"inputs": [],
"name": "NotPendingOwner",
"type": "error"
},
{
"inputs": [],
"name": "Paused",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "windowEndedAt",
"type": "uint64"
}
],
"name": "RedeemWindowExpired",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "SharesToVaultBlocked",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
}
],
"name": "WrongChain",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sharesReturned",
"type": "uint256"
}
],
"name": "CooldownCancelled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sharesCooling",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
}
],
"name": "CooldownRestarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sharesAdded",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalCooling",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
}
],
"name": "CooldownStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "injector",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "InjectorSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "receiver",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sharesBurned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "hissOut",
"type": "uint256"
}
],
"name": "Redeemed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "injector",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "hissIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "carriedUnvested",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint64",
"name": "dripEndsAt",
"type": "uint64"
}
],
"name": "RewardsInjected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "hissIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sharesOut",
"type": "uint256"
}
],
"name": "Staked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "TokenRescued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "VaultPaused",
"type": "event"
},
{
"inputs": [],
"name": "COOLDOWN_SECONDS",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DRIP_SECONDS",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "REDEEM_WINDOW_SECONDS",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ROBINHOOD_CHAIN_ID",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ROBINHOOD_CHAIN_TESTNET_ID",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "cancelCooldown",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "xShares",
"type": "uint256"
}
],
"name": "convertToAssets",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "hissAmount",
"type": "uint256"
}
],
"name": "convertToShares",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "staker",
"type": "address"
}
],
"name": "cooldownOf",
"outputs": [
{
"internalType": "uint256",
"name": "shares",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "windowEndsAt",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "cooldowns",
"outputs": [
{
"internalType": "uint192",
"name": "shares",
"type": "uint192"
},
{
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "dripAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "dripEnd",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "dripStart",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hiss",
"outputs": [
{
"internalType": "contract IERC20Minimal",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hissPerXHiss",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "hissAmount",
"type": "uint256"
}
],
"name": "injectRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isInjector",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "xShares",
"type": "uint256"
}
],
"name": "previewRedeem",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "hissAmount",
"type": "uint256"
}
],
"name": "previewStake",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "xShares",
"type": "uint256"
},
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "redeem",
"outputs": [
{
"internalType": "uint256",
"name": "hissOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "restartCooldown",
"outputs": [
{
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "injector",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setInjector",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "hissAmount",
"type": "uint256"
}
],
"name": "stake",
"outputs": [
{
"internalType": "uint256",
"name": "shares",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "xShares",
"type": "uint256"
}
],
"name": "startCooldown",
"outputs": [
{
"internalType": "uint64",
"name": "readyAt",
"type": "uint64"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalHissStaked",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalXHissSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "unpause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "unvestedRewards",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20Minimal} from "./interfaces/IERC20Minimal.sol";
import {HissOwnable} from "./utils/HissOwnable.sol";
/*//////////////////////////////////////////////////////////////////////////
██╗ ██╗██╗███████╗███████╗ ██╗ ██╗██╗ ██╗██╗███████╗███████╗
██║ ██║██║██╔════╝██╔════╝ ╚██╗██╔╝██║ ██║██║██╔════╝██╔════╝
███████║██║███████╗███████╗ ╚███╔╝ ███████║██║███████╗███████╗
██╔══██║██║╚════██║╚════██║ ██╔██╗ ██╔══██║██║╚════██║╚════██║
██║ ██║██║███████║███████║ ██╔╝ ██╗██║ ██║██║███████║███████║
THIS CONTRACT IS UNAUDITED. Stake HISS, receive xHISS. Fee-funded HISS
injections increase the HISS-per-xHISS share value over time. Not a
performance claim: injections depend entirely on protocol fee flow and
may be zero.
//////////////////////////////////////////////////////////////////////////*/
/// @title XHissVault
/// @notice Single-asset staking vault for $HISS on Robinhood Chain (4663).
/// Stakers deposit HISS and receive xHISS shares; permissioned fee
/// injections raise the HISS-per-xHISS exchange rate for every
/// holder with no per-user claim step. Exits go through a 72h
/// cooldown followed by a 2-day redeem window.
///
/// REFERENCE MODEL — Axiom's xAXIOM StakedAxiom vault
/// (Base, 0x91DA9FAb0b371CAAFd211FE68ab956F774d8ddE8, verified source):
///
/// ADOPTED from xAXIOM:
/// - OZ-style virtual-offset share math for first-depositor inflation
/// resistance (xAXIOM: `_decimalsOffset() = 6`, no dead shares).
/// - The cooldown → bounded redeem window → lapse-requires-recooldown
/// exit flow (xAXIOM live params: 3d cooldown, 2d window; here 72h/2d).
/// - Shares stay live and productive through the cooldown; they are
/// burned only at redeem, at the exchange rate at redeem time (xAXIOM
/// computes via live previewRedeem in its `_withdraw` override).
/// - Exits are never pausable, the stake asset is never rescuable
/// (xAXIOM `rescueToken` reverts for `asset()`), and admin is a
/// two-step ownable (xAXIOM Ownable2Step; here house HissOwnable).
///
/// REJECTED from xAXIOM:
/// - Instant reward repricing. xAXIOM has no reward function at all —
/// rewards arrive as plain ERC-20 donations and `totalAssets()` (raw
/// balance) steps the rate the moment they land. Here rewards enter
/// only through permissioned {injectRewards}, which pulls HISS and
/// stages it as a 24h LINEAR DRIP; {totalHissStaked} excludes the
/// unvested remainder, so the rate rises smoothly instead of stepping.
/// Staged drip + the 72h exit cooldown make injection-sniping
/// (stake right before an injection, exit right after) unprofitable.
/// - The asymmetric offset that inflates share decimals (xAXIOM shares
/// are 24-dec, 1e6 shares per asset). Here the virtual liquidity is
/// SYMMETRIC (1e6 virtual shares AND 1e6 virtual asset wei), keeping
/// xHISS at 18 decimals with a 1:1 initial rate while preserving the
/// same ~1e6x donation-griefing cost bound.
/// - The timestamp-only, whole-balance cooldown with its transfer ban
/// and weighted-average receiver-cooldown rebase. Here a cooldown
/// ESCROWS an explicit share amount in the vault, which supports
/// partial cooldowns, keeps xHISS freely transferable, and removes
/// the rebase edge cases — the escrowed amount is fixed, so topping
/// up mid-cooldown cannot ride an old timer.
/// - Always-open deposits. Here the owner can pause {stake} and
/// {injectRewards} for incident response; see PAUSE SEMANTICS.
///
/// PAUSE SEMANTICS: pause blocks new stakes and new injections only.
/// {startCooldown}, {restartCooldown}, {cancelCooldown}, and {redeem} all
/// work while paused — this is deliberately stronger than "matured
/// cooldowns stay redeemable": a pause during an incident must never trap
/// depositors who have not yet started their exit clock, and an unpaused
/// exit path has no admin lever that could be abused to hold funds.
///
/// DONATIONS: HISS sent directly to this contract (bypassing
/// {injectRewards}) vests instantly into the rate, exactly like xAXIOM's
/// donation model. That is a benign gift path — it cannot be sniped
/// profitably because exits still require the 72h cooldown — and keeping
/// {totalHissStaked} balance-based means no HISS can ever be stranded.
///
/// ADMIN MODEL: owner is the deploying Bankr EOA — the same temporary
/// single-EOA admin model as the rest of the HISS vault suite, documented
/// in docs/ and intended to migrate to a timelock/multisig via the
/// two-step HissOwnable transfer with no code changes.
contract XHissVault is HissOwnable {
// ---------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------
/// @notice Robinhood Chain mainnet / testnet chain ids (the only allowed chains).
uint256 public constant ROBINHOOD_CHAIN_ID = 4663;
uint256 public constant ROBINHOOD_CHAIN_TESTNET_ID = 46630;
/// @notice Seconds between {startCooldown} and the opening of the redeem window.
uint64 public constant COOLDOWN_SECONDS = 72 hours;
/// @notice Length of the redeem window once a cooldown matures. After it
/// lapses the escrowed shares need {restartCooldown}.
uint64 public constant REDEEM_WINDOW_SECONDS = 2 days;
/// @notice Every injection (plus any still-unvested remainder) drips
/// linearly into the exchange rate over this period.
uint64 public constant DRIP_SECONDS = 24 hours;
/// @dev Symmetric virtual liquidity (see header): 1e6 virtual shares and
/// 1e6 virtual asset wei in every conversion. Griefing a victim
/// deposit to zero shares costs ~1e6x that deposit in donations,
/// which the attacker cannot recover.
uint256 private constant VIRTUAL_SHARES = 1e6;
uint256 private constant VIRTUAL_ASSETS = 1e6;
/// @dev 18-decimal fixed-point one, for {hissPerXHiss}.
uint256 private constant RATE_ONE = 1e18;
// ---------------------------------------------------------------------
// Immutables / ERC-20 share token metadata
// ---------------------------------------------------------------------
/// @notice The staked asset: $HISS (18 decimals, fixed supply).
IERC20Minimal public immutable hiss;
string public constant name = "Staked HISS";
string public constant symbol = "xHISS";
uint8 public constant decimals = 18;
// ---------------------------------------------------------------------
// Storage
// ---------------------------------------------------------------------
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/// @notice Total of the active drip window (injected + carried unvested).
uint256 public dripAmount;
/// @notice Start timestamp of the active drip window.
uint64 public dripStart;
/// @notice End timestamp of the active drip window (0 = never injected).
uint64 public dripEnd;
/// @notice One cooldown slot per address. `shares` are escrowed in the
/// vault (still earning) until redeemed or cancelled.
struct Cooldown {
uint192 shares;
uint64 readyAt;
}
mapping(address => Cooldown) public cooldowns;
/// @notice Addresses (besides the owner) allowed to call {injectRewards}.
mapping(address => bool) public isInjector;
/// @notice Pauses {stake} and {injectRewards} only — never exits.
bool public paused;
uint256 private _locked = 1; // 1 = unlocked, 2 = entered
// ---------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Staked(address indexed staker, uint256 hissIn, uint256 sharesOut);
event RewardsInjected(address indexed injector, uint256 hissIn, uint256 carriedUnvested, uint64 dripEndsAt);
event CooldownStarted(address indexed staker, uint256 sharesAdded, uint256 totalCooling, uint64 readyAt);
event CooldownRestarted(address indexed staker, uint256 sharesCooling, uint64 readyAt);
event CooldownCancelled(address indexed staker, uint256 sharesReturned);
event Redeemed(address indexed staker, address indexed receiver, uint256 sharesBurned, uint256 hissOut);
event InjectorSet(address indexed injector, bool approved);
event VaultPaused(bool paused);
event TokenRescued(address indexed token, address indexed to, uint256 amount);
// ---------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------
error WrongChain(uint256 chainId);
error Paused();
error NotPaused();
error NotInjector();
error ZeroAmount();
error Reentrancy();
error TransferFailed();
error InsufficientBalance();
error InsufficientAllowance();
error NoStakers();
error NoCooldown();
error CooldownNotElapsed(uint64 readyAt);
error RedeemWindowExpired(uint64 windowEndedAt);
error InsufficientCooldownShares(uint256 requested, uint256 cooling);
error CannotRescueStakeAsset();
error SharesToVaultBlocked();
// ---------------------------------------------------------------------
// Modifiers
// ---------------------------------------------------------------------
modifier nonReentrant() {
if (_locked != 1) revert Reentrancy();
_locked = 2;
_;
_locked = 1;
}
modifier whenNotPaused() {
if (paused) revert Paused();
_;
}
// ---------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------
/// @param hiss_ The $HISS token (18 decimals, fixed supply).
/// @param initialOwner The admin (deploying Bankr EOA; timelock-ready via HissOwnable).
constructor(address hiss_, address initialOwner) HissOwnable(initialOwner) {
if (block.chainid != ROBINHOOD_CHAIN_ID && block.chainid != ROBINHOOD_CHAIN_TESTNET_ID) {
revert WrongChain(block.chainid);
}
if (hiss_ == address(0)) revert ZeroAddress();
hiss = IERC20Minimal(hiss_);
}
// ---------------------------------------------------------------------
// Exchange rate / views
// ---------------------------------------------------------------------
/// @notice HISS that has been injected but has not yet dripped into the
/// exchange rate (linear over the active window).
function unvestedRewards() public view returns (uint256) {
uint64 end = dripEnd;
if (block.timestamp >= end) return 0;
return dripAmount * (end - block.timestamp) / (end - dripStart);
}
/// @notice HISS currently backing xHISS: the vault's balance minus the
/// unvested remainder of the active drip.
function totalHissStaked() public view returns (uint256) {
return hiss.balanceOf(address(this)) - unvestedRewards();
}
/// @notice Outstanding xHISS supply (includes cooldown-escrowed shares).
function totalXHissSupply() external view returns (uint256) {
return totalSupply;
}
/// @notice HISS → xHISS at the current rate (rounds down; symmetric
/// virtual liquidity, see header).
function convertToShares(uint256 hissAmount) public view returns (uint256) {
return hissAmount * (totalSupply + VIRTUAL_SHARES) / (totalHissStaked() + VIRTUAL_ASSETS);
}
/// @notice xHISS → HISS at the current rate (rounds down).
function convertToAssets(uint256 xShares) public view returns (uint256) {
return xShares * (totalHissStaked() + VIRTUAL_ASSETS) / (totalSupply + VIRTUAL_SHARES);
}
/// @notice xHISS minted for staking `hissAmount` right now.
function previewStake(uint256 hissAmount) external view returns (uint256) {
return convertToShares(hissAmount);
}
/// @notice HISS returned for redeeming `xShares` right now. The actual
/// redeem rate is the rate at redeem time, after the cooldown.
function previewRedeem(uint256 xShares) external view returns (uint256) {
return convertToAssets(xShares);
}
/// @notice Virtual-free exchange rate: HISS (18d) per 1e18 xHISS.
/// Starts at 1e18 and rises as injections vest.
function hissPerXHiss() external view returns (uint256) {
if (totalSupply == 0) return RATE_ONE;
return totalHissStaked() * RATE_ONE / totalSupply;
}
/// @notice Cooldown slot for `staker`: escrowed shares, when the redeem
/// window opens, and when it closes (0s when no cooldown).
function cooldownOf(address staker) external view returns (uint256 shares, uint64 readyAt, uint64 windowEndsAt) {
Cooldown storage c = cooldowns[staker];
shares = c.shares;
readyAt = c.readyAt;
windowEndsAt = c.shares == 0 ? 0 : c.readyAt + REDEEM_WINDOW_SECONDS;
}
// ---------------------------------------------------------------------
// Stake
// ---------------------------------------------------------------------
/// @notice Pulls `hissAmount` HISS and mints xHISS at the current rate.
/// Mid-drip stakes price off the vested-so-far rate, so a staker
/// never buys the already-injected but unvested remainder for free.
function stake(uint256 hissAmount) external nonReentrant whenNotPaused returns (uint256 shares) {
if (hissAmount == 0) revert ZeroAmount();
uint256 stakedBefore = totalHissStaked();
uint256 received = _pullHiss(hissAmount);
shares = received * (totalSupply + VIRTUAL_SHARES) / (stakedBefore + VIRTUAL_ASSETS);
if (shares == 0) revert ZeroAmount();
_mint(msg.sender, shares);
emit Staked(msg.sender, received, shares);
}
// ---------------------------------------------------------------------
// Reward injection (staged 24h linear drip)
// ---------------------------------------------------------------------
/// @notice Pulls `hissAmount` HISS from the caller and stages it — plus
/// any still-unvested remainder of the previous injection — as a
/// fresh 24h linear drip into the exchange rate. Owner or an
/// approved injector only. Reverts when nobody is staked so an
/// injection can never accrue to a future first staker.
function injectRewards(uint256 hissAmount) external nonReentrant whenNotPaused {
if (msg.sender != owner && !isInjector[msg.sender]) revert NotInjector();
if (hissAmount == 0) revert ZeroAmount();
if (totalSupply == 0) revert NoStakers();
uint256 carried = unvestedRewards();
uint256 received = _pullHiss(hissAmount);
dripAmount = carried + received;
dripStart = uint64(block.timestamp);
dripEnd = uint64(block.timestamp) + DRIP_SECONDS;
emit RewardsInjected(msg.sender, received, carried, dripEnd);
}
/// @notice Approves or revokes a reward injector (e.g. the fee controller).
function setInjector(address injector, bool approved) external onlyOwner {
if (injector == address(0)) revert ZeroAddress();
isInjector[injector] = approved;
emit InjectorSet(injector, approved);
}
// ---------------------------------------------------------------------
// Cooldown / redeem exit
// ---------------------------------------------------------------------
/// @notice Escrows `xShares` into the caller's cooldown slot. The redeem
/// window opens 72h later. Adding shares to an existing slot
/// restarts the clock for the whole slot — a fixed escrowed
/// amount can never ride an older timer (anti-sniping).
/// Escrowed shares keep earning until redeemed.
function startCooldown(uint256 xShares) external nonReentrant returns (uint64 readyAt) {
if (xShares == 0) revert ZeroAmount();
if (xShares > type(uint192).max) revert InsufficientBalance();
_moveShares(msg.sender, address(this), xShares);
Cooldown storage c = cooldowns[msg.sender];
c.shares += uint192(xShares);
readyAt = uint64(block.timestamp) + COOLDOWN_SECONDS;
c.readyAt = readyAt;
emit CooldownStarted(msg.sender, xShares, c.shares, readyAt);
}
/// @notice Restarts the 72h clock on an existing cooldown slot — the
/// required step after a redeem window lapses unused.
function restartCooldown() external returns (uint64 readyAt) {
Cooldown storage c = cooldowns[msg.sender];
if (c.shares == 0) revert NoCooldown();
readyAt = uint64(block.timestamp) + COOLDOWN_SECONDS;
c.readyAt = readyAt;
emit CooldownRestarted(msg.sender, c.shares, readyAt);
}
/// @notice Returns all escrowed shares to the caller's balance and clears
/// the slot. Exiting later requires a fresh 72h cooldown.
function cancelCooldown() external nonReentrant {
Cooldown storage c = cooldowns[msg.sender];
uint256 shares = c.shares;
if (shares == 0) revert NoCooldown();
delete cooldowns[msg.sender];
_moveShares(address(this), msg.sender, shares);
emit CooldownCancelled(msg.sender, shares);
}
/// @notice Burns up to the caller's escrowed shares at the CURRENT rate
/// and pays HISS to `receiver`. Only inside the redeem window
/// [readyAt, readyAt + 2 days]; after that, {restartCooldown}.
/// Works while paused — exits are never pausable.
function redeem(uint256 xShares, address receiver) external nonReentrant returns (uint256 hissOut) {
if (xShares == 0) revert ZeroAmount();
if (receiver == address(0)) revert ZeroAddress();
Cooldown storage c = cooldowns[msg.sender];
if (c.shares == 0) revert NoCooldown();
uint64 readyAt = c.readyAt;
if (block.timestamp < readyAt) revert CooldownNotElapsed(readyAt);
uint64 windowEnd = readyAt + REDEEM_WINDOW_SECONDS;
if (block.timestamp > windowEnd) revert RedeemWindowExpired(windowEnd);
if (xShares > c.shares) revert InsufficientCooldownShares(xShares, c.shares);
hissOut = convertToAssets(xShares);
if (hissOut == 0) revert ZeroAmount();
c.shares -= uint192(xShares);
if (c.shares == 0) delete cooldowns[msg.sender];
_burn(address(this), xShares);
if (!hiss.transfer(receiver, hissOut)) revert TransferFailed();
emit Redeemed(msg.sender, receiver, xShares, hissOut);
}
// ---------------------------------------------------------------------
// Admin
// ---------------------------------------------------------------------
/// @notice Pauses {stake} and {injectRewards}. Never blocks any exit path.
function pause() external onlyOwner {
paused = true;
emit VaultPaused(true);
}
/// @notice Unpauses {stake} and {injectRewards}.
function unpause() external onlyOwner {
if (!paused) revert NotPaused();
paused = false;
emit VaultPaused(false);
}
/// @notice Rescues tokens accidentally sent here — never the stake asset
/// (HISS: staked, dripping, or donated) and never the vault's own
/// xHISS: calling `this.transfer` via rescue would run with
/// `msg.sender == vault` and move cooldown-escrowed shares.
function rescueToken(address token, address to, uint256 amount) external onlyOwner nonReentrant {
if (token == address(hiss) || token == address(this)) revert CannotRescueStakeAsset();
if (to == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (!IERC20Minimal(token).transfer(to, amount)) revert TransferFailed();
emit TokenRescued(token, to, amount);
}
// ---------------------------------------------------------------------
// ERC-20 share token
// ---------------------------------------------------------------------
function transfer(address to, uint256 value) external returns (bool) {
_checkedMove(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed < value) revert InsufficientAllowance();
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - value;
_checkedMove(from, to, value);
return true;
}
/// @dev External transfers may never target the vault itself: escrow is
/// only entered through {startCooldown}, so escrow accounting always
/// equals the vault's own share balance.
function _checkedMove(address from, address to, uint256 value) private {
if (to == address(0)) revert ZeroAddress();
if (to == address(this)) revert SharesToVaultBlocked();
_moveShares(from, to, value);
}
function _moveShares(address from, address to, uint256 value) private {
if (balanceOf[from] < value) revert InsufficientBalance();
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
}
function _mint(address to, uint256 value) private {
totalSupply += value;
balanceOf[to] += value;
emit Transfer(address(0), to, value);
}
/// @dev Only ever called on the vault's own escrow, whose balance equals
/// the sum of cooldown slots (see {_checkedMove}); 0.8 checked math
/// still reverts on underflow if that invariant were ever broken.
function _burn(address from, uint256 value) private {
balanceOf[from] -= value;
totalSupply -= value;
emit Transfer(from, address(0), value);
}
/// @dev Pulls HISS and returns the amount actually received (balance
/// delta), so share math stays exact even for nonstandard tokens.
function _pullHiss(uint256 amount) private returns (uint256 received) {
uint256 before = hiss.balanceOf(address(this));
if (!hiss.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
received = hiss.balanceOf(address(this)) - before;
if (received == 0) revert ZeroAmount();
}
}
Chain explorer3526msChain node84ms