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
Creation tx
0x93bd29c657…1a63620609

Token

Name
Staked HISS
Symbol
xHISS
Decimals
18
Holders
7

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