StonkNFTAMMVault

0xe302733accf4800146e55fc45b46b4e4ffc032d2

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,658 bytes
ABI entries
88
41 read · 17 write
License
none

Contract information

Address
0xe302733accf4800146e55fc45b46b4e4ffc032d2
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xbbee0cf552…c7b85669e8

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (41)

BOOSTER_SHARE_BPS()uint16
DEFAULT_ADMIN_ROLE()bytes32
GOVERNOR_ROLE()bytes32
INGEST_ROLE()bytes32
MAX_FEE_BPS()uint16
PAUSER_ROLE()bytes32
collection()address
escrow()address
ethNotionalPerNFT()uint256
feeExempt(address)bool
feeOracleStatus()bool, bool, uint256, uint256, uint256, uint32, uint256
getNextTokenId(uint256)uint256
getRoleAdmin(bytes32)bytes32
hasRole(bytes32, address)bool
inventory(uint256)bool
inventoryCount()uint256
inventoryLength()uint256
liveEthNotionalPerNFT()uint256
maxNotionalWei()uint256
minNotionalWei()uint256
newestTokenId()uint256
oldestTokenId()uint256
paused()bool
protocolTreasury()address
quoteRandomBuy()uint256, uint256, uint256, uint256
quoteSellNFT()uint256, uint256
quoteSpecificBuy(uint256)uint256, uint256, bool
randomFeeBps()uint16
specificFeeBps()uint16
spikeCapBps()uint16
stockBooster()address
supportsInterface(bytes4)bool
token()address
tokensPerNFT()uint256
twapFastWindow()uint32
twapMidPool()address
twapMidToken()address
twapPool()address
twapQuoteToken()address
twapWindow()uint32
twapWiredAt()uint64

Events (13)

EthNotionalUpdatedFeeExemptUpdatedFeesUpdatedNFTBoughtNFTSoldNotionalBoundsUpdatedPausedRoleAdminChangedRoleGrantedRoleRevokedTwapFastWindowUpdatedTwapOracleUpdatedUnpaused

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "collection_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "escrow_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "stockBooster_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "protocolTreasury_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensPerNFT_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethNotionalPerNFT_",
        "type": "uint256"
      },
      {
        "internalType": "uint16",
        "name": "randomFeeBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "specificFeeBps_",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AccessControlBadConfirmation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "neededRole",
        "type": "bytes32"
      }
    ],
    "name": "AccessControlUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EmptyInventory",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientEthFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRandomFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidSpecificFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotInInventory",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "T",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldNotionalWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newNotionalWei",
        "type": "uint256"
      }
    ],
    "name": "EthNotionalUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "exempt",
        "type": "bool"
      }
    ],
    "name": "FeeExemptUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "randomFeeBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "specificFeeBps",
        "type": "uint16"
      }
    ],
    "name": "FeesUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethFeePaid",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "boosterShare",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolShare",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isSpecific",
        "type": "bool"
      }
    ],
    "name": "NFTBought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethFeePaid",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "boosterShare",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolShare",
        "type": "uint256"
      }
    ],
    "name": "NFTSold",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "minWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "maxWei",
        "type": "uint256"
      }
    ],
    "name": "NotionalBoundsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "previousAdminRole",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "newAdminRole",
        "type": "bytes32"
      }
    ],
    "name": "RoleAdminChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleGranted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleRevoked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint32",
        "name": "fastWindow",
        "type": "uint32"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "capBps",
        "type": "uint16"
      }
    ],
    "name": "TwapFastWindowUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "quoteToken",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "midPool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "midToken",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint32",
        "name": "window",
        "type": "uint32"
      }
    ],
    "name": "TwapOracleUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BOOSTER_SHARE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GOVERNOR_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "INGEST_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PAUSER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "maxCost",
        "type": "uint256"
      }
    ],
    "name": "buyRandomNFT",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxCost",
        "type": "uint256"
      }
    ],
    "name": "buySpecificNFT",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "collection",
    "outputs": [
      {
        "internalType": "contract IERC721",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "escrow",
    "outputs": [
      {
        "internalType": "contract ITokenEscrowReserve",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ethNotionalPerNFT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "feeExempt",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeOracleStatus",
    "outputs": [
      {
        "internalType": "bool",
        "name": "wired",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "live",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "notionalWei",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "swapFeeWei",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "snipeFeeWei",
        "type": "uint256"
      },
      {
        "internalType": "uint32",
        "name": "window",
        "type": "uint32"
      },
      {
        "internalType": "uint256",
        "name": "readyAt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getNextTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      }
    ],
    "name": "getRoleAdmin",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "grantRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "hasRole",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ingestNFT",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "inventory",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "inventoryCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "inventoryLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "liveEthNotionalPerNFT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxNotionalWei",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minNotionalWei",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "newestTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "oldestTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "name": "onERC721Received",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "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": "protocolTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "quoteRandomBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenCost",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethFee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "inventorySize",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "nextTokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "quoteSellNFT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenPayout",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "quoteSpecificBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenCost",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethFee",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "available",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "randomFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "callerConfirmation",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "rescueEth",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "revokeRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minPayout",
        "type": "uint256"
      }
    ],
    "name": "sellNFT",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newNotionalWei",
        "type": "uint256"
      }
    ],
    "name": "setEthNotionalPerNFT",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "exempt",
        "type": "bool"
      }
    ],
    "name": "setFeeExempt",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "randomFeeBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "specificFeeBps_",
        "type": "uint16"
      }
    ],
    "name": "setFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minWei",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxWei",
        "type": "uint256"
      }
    ],
    "name": "setNotionalBounds",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint32",
        "name": "fastWindow",
        "type": "uint32"
      },
      {
        "internalType": "uint16",
        "name": "capBps",
        "type": "uint16"
      }
    ],
    "name": "setTwapFastWindow",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "quoteToken",
        "type": "address"
      },
      {
        "internalType": "uint32",
        "name": "window",
        "type": "uint32"
      },
      {
        "internalType": "address",
        "name": "midPool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "midToken",
        "type": "address"
      }
    ],
    "name": "setTwapOracle",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "specificFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "spikeCapBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stockBooster",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokensPerNFT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapFastWindow",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapMidPool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapMidToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapPool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapQuoteToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapWindow",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "twapWiredAt",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {ERC721Holder} from "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {Pausable} from "openzeppelin-contracts/contracts/utils/Pausable.sol";
import {AccessControl} from "openzeppelin-contracts/contracts/access/AccessControl.sol";
import {ITokenEscrowReserve} from "../interfaces/ITokenEscrowReserve.sol";
import {TwapQuote} from "../libs/TwapQuote.sol";
import {
    ZeroAddress,
    EmptyInventory,
    NotInInventory,
    SlippageExceeded,
    InvalidRandomFee,
    InvalidSpecificFee,
    InsufficientEthFee,
    EthTransferFailed,
    InvalidConfig
} from "../libs/Errors.sol";
import {Events} from "../libs/Events.sol";

/// @dev Token surface of the STONKBROKER/WETH v3 pool needed to validate oracle wiring.
interface ITwapPoolTokens {
    function token0() external view returns (address);
    function token1() external view returns (address);
}

/// @title StonkNFTAMMVault
/// @notice StonkBrokers fork of Anvil's NFTAMMVault with one deliberate economic change:
/// trade fees are charged in native ETH instead of as a cut of the collection token, and the
/// collected ETH is split 70% to the StockBooster (which swaps it into stock tokens and
/// airdrops them to activated brokers' token-bound wallets) and 30% to the protocol treasury.
///
/// All of Anvil's token-denominated fee buckets are zeroed for this market:
///  - no staker fee cut of the collection token (there is no NFTStakingVault in this market),
///  - no protocol treasury bps, no burn bps.
/// Buys and sells therefore move exactly `tokensPerNFT` of $STONKBROKER against the escrow;
/// the only levy anywhere is the ETH fee computed as bps of the ETH NOTIONAL of one NFT.
///
/// The notional is LIVE by default (per launch decision — the fee must track real market
/// value, not a hand-set peg): once the governor wires the STONKBROKER/WETH pool via
/// setTwapOracle(), every fee is bps of the TWAP value of `tokensPerNFT` over `twapWindow`
/// seconds, clamped to governor-set [minNotionalWei, maxNotionalWei] bounds. The stored
/// `ethNotionalPerNFT` then serves as the fail-open fallback: every successful oracle read
/// refreshes it (last-good cache), and any oracle failure — pool migrated, cardinality too
/// low, observe revert — silently falls back to it, so a trade can NEVER fail because of
/// the fee oracle. Before the oracle is wired (seeding), the stored value alone is the fee
/// base, and 0 means fee-free.
///
/// Two trade classes, both fee-split 70/30 booster/protocol:
///  - SWAP  (`sellNFT` / `buyRandomNFT`)  — charged `randomFeeBps`   (launch: 1000 = 10%)
///  - SNIPE (`buySpecificNFT`)            — charged `specificFeeBps` (launch: 1500 = 15%,
///    the MAX_FEE_BPS ceiling — picking an exact broker out of inventory costs a premium).
///
/// Inventory management (FIFO doubly-linked list), escrow interaction, pause and ingest paths
/// are inherited from the audited Anvil vault design unchanged.
contract StonkNFTAMMVault is ERC721Holder, ReentrancyGuard, Pausable, AccessControl {
    using SafeERC20 for IERC20;

    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
    bytes32 public constant INGEST_ROLE = keccak256("INGEST_ROLE");

    uint16 public constant MAX_FEE_BPS = 1500; // 15% hard cap, same as Anvil
    /// @notice Booster share of every ETH fee; remainder (including rounding dust) goes to protocol.
    uint16 public constant BOOSTER_SHARE_BPS = 7000; // 70%

    IERC20 public immutable token;
    IERC721 public immutable collection;
    ITokenEscrowReserve public immutable escrow;

    /// @notice Receives 70% of every ETH trade fee (the StockBooster).
    address public immutable stockBooster;
    /// @notice Receives 30% of every ETH trade fee.
    address public immutable protocolTreasury;

    uint256 public immutable tokensPerNFT;

    /// @notice Fallback/bootstrap ETH notional of one NFT. Primary fee base only until the
    /// TWAP oracle is wired; afterwards it is the fail-open cache, auto-refreshed by every
    /// successful oracle read during a trade. Launch policy: NEVER zero — admin seeding is
    /// handled per-address via `feeExempt`, not by turning fees off globally.
    /// Example: notional 0.2 ETH, randomFeeBps 500 => 0.01 ETH fee.
    uint256 public ethNotionalPerNFT;
    uint16 public randomFeeBps;
    uint16 public specificFeeBps;

    /// @notice v3-style pool the live notional is read from (0 = oracle off). Either the
    /// direct STONKBROKER/WETH pool, or — when `twapMidPool` is set — the STONKBROKER/mid
    /// leg of a two-hop route (e.g. STONKBROKER/USDG + USDG/WETH), so the oracle survives a
    /// liquidity migration to a non-WETH main pair.
    address public twapPool;
    /// @notice The token the notional is quoted in (WETH — fees are charged in native ETH).
    address public twapQuoteToken;
    /// @notice Optional second hop: pool holding `twapMidToken`/`twapQuoteToken` (0 = direct).
    address public twapMidPool;
    /// @notice Intermediate token of the two-hop route (e.g. USDG). 0 when direct.
    address public twapMidToken;
    /// @notice TWAP averaging window in seconds, applied to BOTH hops. Longer = more
    /// manipulation-resistant, slower to track real moves. Launch target: 1800 (30 min).
    uint32 public twapWindow;
    /// @notice Optional FAST window (launch target: 300s / 5 min) for spike capture. During
    /// a rally the slow TWAP lags and undercharges; when set, the fee base is
    ///   max(slow, min(fast, slow * spikeCapBps / 1e4))
    /// so genuine spikes are captured within ~one fast window instead of one slow window,
    /// while manipulation stays bounded: pushing the price DOWN can never cut fees below the
    /// slow average (outer max), and pushing it UP inflates fees at most spikeCapBps beyond
    /// the slow average (inner min) — at real cost against arbitrage. 0 = disabled.
    /// The 5-minute averaging also keeps quotes stable for users: the fee a wallet displays
    /// at signing time cannot jump materially in the seconds before inclusion, because even
    /// a violent spot move only enters the fee base at 1/300th per second.
    uint32 public twapFastWindow;
    /// @notice Ceiling on the fast window's influence, in bps of the slow TWAP (e.g. 30000 =
    /// fast can raise the fee base to at most 3x slow). Must be >= 1e4 when fast is enabled.
    uint16 public spikeCapBps;
    /// @notice Timestamp the oracle was (re)wired — anchors the UI's warmup countdown
    /// (readyAt ≈ twapWiredAt + twapWindow). Purely informational; `live` in
    /// feeOracleStatus() is the authoritative signal.
    uint64 public twapWiredAt;
    /// @notice Clamp bounds on the oracle-derived notional (wei). A manipulated or broken
    /// pool can only move the fee (and the synced fallback cache) within this governor-set
    /// band, which is why a non-zero maxNotionalWei is REQUIRED while the oracle is live.
    uint256 public minNotionalWei;
    uint256 public maxNotionalWei;

    /// @notice Addresses that trade with a ZERO ETH fee — the admin seeding switch. Fees are
    /// live for everyone from block one; the deployer/admin is exempted just long enough to
    /// seed AMM inventory and the LP, then the exemption is revoked. Per-address (not a
    /// global off switch) so seeding can never accidentally open a fee-free window for the
    /// public, and revocation is one call that can't be front-run into free trades by others.
    mapping(address => bool) public feeExempt;

    mapping(uint256 => bool) public inventory;
    mapping(uint256 => uint256) private _nextTokenId;
    mapping(uint256 => uint256) private _prevTokenId;
    uint256 public oldestTokenId;
    uint256 public newestTokenId;
    uint256 public inventoryCount;

    constructor(
        address token_,
        address collection_,
        address escrow_,
        address stockBooster_,
        address protocolTreasury_,
        uint256 tokensPerNFT_,
        uint256 ethNotionalPerNFT_,
        uint16 randomFeeBps_,
        uint16 specificFeeBps_
    ) {
        if (
            token_ == address(0) || collection_ == address(0) || escrow_ == address(0) || stockBooster_ == address(0)
                || protocolTreasury_ == address(0)
        ) revert ZeroAddress();
        if (randomFeeBps_ > MAX_FEE_BPS) revert InvalidRandomFee();
        if (specificFeeBps_ > MAX_FEE_BPS) revert InvalidSpecificFee();
        if (specificFeeBps_ < randomFeeBps_) revert InvalidSpecificFee();

        token = IERC20(token_);
        collection = IERC721(collection_);
        escrow = ITokenEscrowReserve(escrow_);
        stockBooster = stockBooster_;
        protocolTreasury = protocolTreasury_;
        tokensPerNFT = tokensPerNFT_;
        ethNotionalPerNFT = ethNotionalPerNFT_;
        randomFeeBps = randomFeeBps_;
        specificFeeBps = specificFeeBps_;

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(PAUSER_ROLE, msg.sender);
    }

    /// @notice Sell one NFT into the vault for the full `tokensPerNFT` payout. The seller pays
    /// the ETH trade fee via msg.value (excess refunded); the token payout itself is uncut.
    /// @dev Strict CEI: the fee is CHECKED up front, all token/NFT effects complete, and only
    /// then is ETH distributed/refunded (external calls last, on top of nonReentrant).
    function sellNFT(uint256 tokenId, uint256 minPayout) external payable nonReentrant whenNotPaused {
        uint256 ethFee = _ethFeeAndSync(randomFeeBps);
        if (msg.value < ethFee) revert InsufficientEthFee();

        uint256 payout = tokensPerNFT;
        if (payout < minPayout) revert SlippageExceeded();

        collection.safeTransferFrom(msg.sender, address(this), tokenId);
        _pushInventory(tokenId);

        escrow.release(address(this), payout, ITokenEscrowReserve.BucketType.AMM);
        token.safeTransfer(msg.sender, payout);

        (uint256 boosterShare, uint256 protocolShare) = _distributeEthFee(ethFee);
        emit Events.NFTSold(msg.sender, tokenId, payout, ethFee, boosterShare, protocolShare);
    }

    /// @notice Buy the oldest NFT in inventory for exactly `tokensPerNFT` plus the ETH fee.
    function buyRandomNFT(uint256 maxCost) external payable nonReentrant whenNotPaused {
        if (inventoryCount == 0) revert EmptyInventory();

        uint256 ethFee = _ethFeeAndSync(randomFeeBps);
        if (msg.value < ethFee) revert InsufficientEthFee();

        uint256 cost = tokensPerNFT;
        if (cost > maxCost) revert SlippageExceeded();

        token.safeTransferFrom(msg.sender, address(this), cost);
        token.forceApprove(address(escrow), cost);
        escrow.returnTokens(cost, ITokenEscrowReserve.BucketType.AMM);

        uint256 tokenId = oldestTokenId;
        _removeInventory(tokenId);
        collection.safeTransferFrom(address(this), msg.sender, tokenId);

        (uint256 boosterShare, uint256 protocolShare) = _distributeEthFee(ethFee);
        emit Events.NFTBought(msg.sender, tokenId, cost, ethFee, boosterShare, protocolShare, false);
    }

    /// @notice Buy a specific NFT out of inventory; uses the higher `specificFeeBps` ETH fee.
    function buySpecificNFT(uint256 tokenId, uint256 maxCost) external payable nonReentrant whenNotPaused {
        if (!inventory[tokenId]) revert NotInInventory();

        uint256 ethFee = _ethFeeAndSync(specificFeeBps);
        if (msg.value < ethFee) revert InsufficientEthFee();

        uint256 cost = tokensPerNFT;
        if (cost > maxCost) revert SlippageExceeded();

        token.safeTransferFrom(msg.sender, address(this), cost);
        token.forceApprove(address(escrow), cost);
        escrow.returnTokens(cost, ITokenEscrowReserve.BucketType.AMM);

        _removeInventory(tokenId);
        collection.safeTransferFrom(address(this), msg.sender, tokenId);

        (uint256 boosterShare, uint256 protocolShare) = _distributeEthFee(ethFee);
        emit Events.NFTBought(msg.sender, tokenId, cost, ethFee, boosterShare, protocolShare, true);
    }

    // ------------------------------------------------------------------
    // Quotes
    // ------------------------------------------------------------------

    function quoteRandomBuy()
        external
        view
        returns (uint256 tokenCost, uint256 ethFee, uint256 inventorySize, uint256 nextTokenId)
    {
        tokenCost = tokensPerNFT;
        ethFee = _ethFee(randomFeeBps);
        inventorySize = inventoryCount;
        nextTokenId = oldestTokenId;
    }

    function quoteSpecificBuy(uint256 tokenId)
        external
        view
        returns (uint256 tokenCost, uint256 ethFee, bool available)
    {
        tokenCost = tokensPerNFT;
        ethFee = _ethFee(specificFeeBps);
        available = inventory[tokenId];
    }

    function quoteSellNFT() external view returns (uint256 tokenPayout, uint256 ethFee) {
        tokenPayout = tokensPerNFT;
        ethFee = _ethFee(randomFeeBps);
    }

    /// @notice One-call UI read for the fee oracle: what fees cost right now, whether they
    /// come from the live TWAP or the fixed launch rate, and — during warmup — when the
    /// oracle is expected to take over.
    /// @return wired      oracle configured (pool set)
    /// @return live       this block's notional came from the TWAP (false = fixed fallback,
    ///                    the launch mode / outage mode)
    /// @return notionalWei current fee base (live TWAP value of tokensPerNFT, clamped; or
    ///                    the fallback)
    /// @return swapFeeWei  ETH fee for sells / random buys right now
    /// @return snipeFeeWei ETH fee for specific buys right now
    /// @return window      TWAP averaging window (seconds) — the fee is a rolling average
    ///                    over this period, re-computed every block
    /// @return readyAt     estimated timestamp the TWAP starts answering (wiredAt + window);
    ///                    0 once live or when not wired. A UI hint only — `live` is the
    ///                    authoritative flag and can flip true earlier if the pool already
    ///                    had history when wired.
    function feeOracleStatus()
        external
        view
        returns (
            bool wired,
            bool live,
            uint256 notionalWei,
            uint256 swapFeeWei,
            uint256 snipeFeeWei,
            uint32 window,
            uint256 readyAt
        )
    {
        wired = twapPool != address(0);
        (notionalWei, live) = _liveNotional();
        swapFeeWei = (notionalWei * randomFeeBps) / 10_000;
        snipeFeeWei = (notionalWei * specificFeeBps) / 10_000;
        window = twapWindow;
        if (wired && !live) {
            uint256 estimate = uint256(twapWiredAt) + window;
            readyAt = estimate > block.timestamp ? estimate : 0;
        }
    }

    function inventoryLength() external view returns (uint256) {
        return inventoryCount;
    }

    /// @notice Returns the next tokenId in the FIFO linked list (decoded). Returns 0 if tail.
    function getNextTokenId(uint256 tokenId) external view returns (uint256) {
        uint256 encoded = _nextTokenId[tokenId];
        return encoded == 0 ? 0 : _decodeTokenId(encoded);
    }

    // ------------------------------------------------------------------
    // Admin
    // ------------------------------------------------------------------

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
        _unpause();
    }

    function setFees(uint16 randomFeeBps_, uint16 specificFeeBps_) external onlyRole(GOVERNOR_ROLE) {
        if (randomFeeBps_ > MAX_FEE_BPS) revert InvalidRandomFee();
        if (specificFeeBps_ > MAX_FEE_BPS) revert InvalidSpecificFee();
        if (specificFeeBps_ < randomFeeBps_) revert InvalidSpecificFee();

        randomFeeBps = randomFeeBps_;
        specificFeeBps = specificFeeBps_;

        emit Events.FeesUpdated(address(this), randomFeeBps_, specificFeeBps_);
    }

    /// @notice Grants/revokes a zero-ETH-fee exemption for one address — the admin seeding
    /// switch. Turn on for the seeding wallet, seed AMM inventory + LP, turn off. Fees for
    /// everyone else are never affected.
    function setFeeExempt(address account, bool exempt) external onlyRole(GOVERNOR_ROLE) {
        if (account == address(0)) revert ZeroAddress();
        feeExempt[account] = exempt;
        emit Events.FeeExemptUpdated(address(this), account, exempt);
    }

    /// @notice Updates the fallback ETH notional per NFT — the fixed launch fee base until
    /// the oracle warms up, and the outage fallback after (successful reads overwrite it).
    /// Launch policy: keep this non-zero; admin seeding uses setFeeExempt instead of zeroing
    /// fees for everyone.
    function setEthNotionalPerNFT(uint256 newNotionalWei) external onlyRole(GOVERNOR_ROLE) {
        emit Events.EthNotionalUpdated(address(this), ethNotionalPerNFT, newNotionalWei);
        ethNotionalPerNFT = newNotionalWei;
    }

    /// @notice Wires (or re-points / disables with pool=0) the live-notional TWAP oracle.
    /// Direct route: pool = STONKBROKER/quoteToken, midPool = midToken = 0. Two-hop route
    /// (main LP not paired with WETH, e.g. STONKBROKER/USDG): pool = STONKBROKER/midToken and
    /// midPool = midToken/quoteToken. Every leg's token pair is validated on-chain so a
    /// fat-fingered pool address can never silently misprice fees, and re-pointing after a
    /// liquidity migration is this one governor call — trades never stop, because any gap or
    /// unwarmed pool just serves the fallback notional.
    ///
    /// A clamp band (setNotionalBounds with maxWei != 0) is REQUIRED first: it is the blast-
    /// radius limit that keeps even a successfully manipulated pool from runaway fees or
    /// poisoning the fallback cache. Prerequisite before the oracle answers:
    /// increaseObservationCardinalityNext on each pool and one full window of history.
    function setTwapOracle(address pool, address quoteToken, uint32 window, address midPool, address midToken)
        external
        onlyRole(GOVERNOR_ROLE)
    {
        if (pool == address(0)) {
            twapPool = address(0);
            twapQuoteToken = address(0);
            twapMidPool = address(0);
            twapMidToken = address(0);
            twapWindow = 0;
            twapWiredAt = 0;
            emit Events.TwapOracleUpdated(address(this), address(0), address(0), address(0), address(0), 0);
            return;
        }
        if (quoteToken == address(0) || window == 0) revert InvalidConfig();
        if (tokensPerNFT > type(uint128).max) revert InvalidConfig();
        if (maxNotionalWei == 0) revert InvalidConfig(); // band must exist while oracle is live
        if (twapFastWindow != 0 && twapFastWindow >= window) revert InvalidConfig();

        if (midPool == address(0)) {
            if (midToken != address(0)) revert InvalidConfig();
            _requirePair(pool, address(token), quoteToken);
        } else {
            if (midToken == address(0)) revert InvalidConfig();
            _requirePair(pool, address(token), midToken);
            _requirePair(midPool, midToken, quoteToken);
        }

        twapPool = pool;
        twapQuoteToken = quoteToken;
        twapMidPool = midPool;
        twapMidToken = midToken;
        twapWindow = window;
        twapWiredAt = uint64(block.timestamp);
        emit Events.TwapOracleUpdated(address(this), pool, quoteToken, midPool, midToken, window);
    }

    /// @dev Reverts unless `pool` holds exactly the {a, b} pair (either order).
    function _requirePair(address pool, address a, address b) internal view {
        address t0 = ITwapPoolTokens(pool).token0();
        address t1 = ITwapPoolTokens(pool).token1();
        if (!((t0 == a && t1 == b) || (t1 == a && t0 == b))) revert InvalidConfig();
    }

    /// @notice Governor band the oracle notional is clamped into — the blast-radius limit for
    /// pool manipulation or a mispriced pool. While the oracle is live the upper clamp is
    /// mandatory (maxWei != 0); it can only be lifted after disabling the oracle.
    function setNotionalBounds(uint256 minWei, uint256 maxWei) external onlyRole(GOVERNOR_ROLE) {
        if (maxWei != 0 && minWei > maxWei) revert InvalidConfig();
        if (maxWei == 0 && twapPool != address(0)) revert InvalidConfig();
        minNotionalWei = minWei;
        maxNotionalWei = maxWei;
        emit Events.NotionalBoundsUpdated(address(this), minWei, maxWei);
    }

    /// @notice Configures spike capture: a fast TWAP window that lets the fee base track
    /// genuine rallies within ~fastWindow instead of ~twapWindow. fastWindow = 0 disables.
    /// The fast window must be strictly shorter than the slow window, and capBps >= 1e4 so
    /// the fast leg can only ever RAISE the fee base above the slow floor, never cut it.
    function setTwapFastWindow(uint32 fastWindow, uint16 capBps) external onlyRole(GOVERNOR_ROLE) {
        if (fastWindow != 0) {
            if (capBps < 10_000) revert InvalidConfig();
            if (twapWindow != 0 && fastWindow >= twapWindow) revert InvalidConfig();
        }
        twapFastWindow = fastWindow;
        spikeCapBps = capBps;
        emit Events.TwapFastWindowUpdated(address(this), fastWindow, capBps);
    }

    /// @notice Loan liquidations transfer collateral here then register it via this call.
    function ingestNFT(uint256 tokenId) external onlyRole(INGEST_ROLE) {
        require(collection.ownerOf(tokenId) == address(this), "NFT not held by vault");
        if (inventory[tokenId]) return;
        _pushInventory(tokenId);
    }

    // ------------------------------------------------------------------
    // Internal: ETH fee handling
    // ------------------------------------------------------------------

    /// @notice The ETH notional one NFT is currently worth: live TWAP value of `tokensPerNFT`
    /// (clamped to the governor band) when the oracle answers, else the stored fallback.
    function liveEthNotionalPerNFT() public view returns (uint256) {
        (uint256 notional,) = _liveNotional();
        return notional;
    }

    /// @dev (notional, fromOracle). Never reverts: any oracle failure — either hop — returns
    /// the fallback. When the fast window is enabled and answers, the fee base is
    /// max(slow, min(fast, slow * spikeCapBps / 1e4)); a fast-window failure silently
    /// degrades to slow-only. The clamp band is applied AFTER blending so it bounds what
    /// trades pay and what the fallback cache can be poisoned to.
    function _liveNotional() internal view returns (uint256, bool) {
        if (twapPool != address(0) && twapWindow > 0) {
            (bool ok, uint256 quoted) = _twapNotional(twapWindow);
            if (ok) {
                if (twapFastWindow > 0 && spikeCapBps >= 10_000) {
                    (bool fastOk, uint256 fast) = _twapNotional(twapFastWindow);
                    if (fastOk && fast > quoted) {
                        uint256 cap = (quoted * spikeCapBps) / 10_000;
                        quoted = fast < cap ? fast : cap;
                    }
                }
                if (quoted < minNotionalWei) quoted = minNotionalWei;
                if (maxNotionalWei != 0 && quoted > maxNotionalWei) quoted = maxNotionalWei;
                return (quoted, true);
            }
        }
        return (ethNotionalPerNFT, false);
    }

    /// @dev One full (possibly two-hop) TWAP quote of tokensPerNFT at the given window.
    function _twapNotional(uint32 window) internal view returns (bool ok, uint256 quoted) {
        if (twapMidPool == address(0)) {
            (ok, quoted) = TwapQuote.tryQuote(twapPool, window, uint128(tokensPerNFT), address(token), twapQuoteToken);
        } else {
            // Hop 1: tokensPerNFT -> mid token value.
            (ok, quoted) = TwapQuote.tryQuote(twapPool, window, uint128(tokensPerNFT), address(token), twapMidToken);
            // Hop 2: mid value -> WETH. Oversized hop-1 output (deliberately broken mid
            // pair) fails soft rather than truncating into a wrong fee.
            if (ok && quoted <= type(uint128).max) {
                (ok, quoted) = TwapQuote.tryQuote(twapMidPool, window, uint128(quoted), twapMidToken, twapQuoteToken);
            } else {
                ok = false;
            }
        }
    }

    function _ethFee(uint16 feeBps) internal view returns (uint256) {
        if (feeExempt[msg.sender]) return 0;
        (uint256 notional,) = _liveNotional();
        return (notional * feeBps) / 10_000;
    }

    /// @dev Trade-path variant of _ethFee: additionally refreshes the fallback cache whenever
    /// the oracle answered, so an oracle outage later charges the last true market value
    /// instead of a stale hand-set number. The cache sync runs even for fee-exempt callers
    /// so seeding trades keep the fallback fresh.
    function _ethFeeAndSync(uint16 feeBps) internal returns (uint256) {
        (uint256 notional, bool fromOracle) = _liveNotional();
        if (fromOracle && notional != ethNotionalPerNFT) ethNotionalPerNFT = notional;
        if (feeExempt[msg.sender]) return 0;
        return (notional * feeBps) / 10_000;
    }

    /// @dev Splits the (already-checked) fee 70/30 booster/protocol (dust to protocol),
    /// forwards both shares, and refunds any excess to the caller. Always called as the LAST
    /// step of a trade so every external ETH send happens after all token/NFT state changes
    /// (strict CEI); reentrancy is additionally blocked by nonReentrant on every entry point.
    function _distributeEthFee(uint256 ethFee) internal returns (uint256 boosterShare, uint256 protocolShare) {
        boosterShare = (ethFee * BOOSTER_SHARE_BPS) / 10_000;
        protocolShare = ethFee - boosterShare;

        if (boosterShare > 0) _sendEth(stockBooster, boosterShare);
        if (protocolShare > 0) _sendEth(protocolTreasury, protocolShare);

        uint256 refund = msg.value - ethFee;
        if (refund > 0) _sendEth(msg.sender, refund);
    }

    /// @notice Admin rescue for ETH force-sent to the vault (e.g. selfdestruct). The vault
    /// never holds ETH between transactions in normal operation — trade fees are checked
    /// against msg.value and fully distributed/refunded within the same call — so anything
    /// sitting here is stray by definition and safe to sweep.
    function rescueEth(address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (to == address(0)) revert ZeroAddress();
        _sendEth(to, amount);
    }

    function _sendEth(address to, uint256 amount) internal {
        (bool ok,) = payable(to).call{value: amount}("");
        if (!ok) revert EthTransferFailed();
    }

    // ------------------------------------------------------------------
    // Internal: FIFO inventory (unchanged from Anvil)
    // ------------------------------------------------------------------

    /// @dev Appends tokenId to the tail of the doubly-linked inventory list. Reverts if already tracked.
    function _pushInventory(uint256 tokenId) internal {
        require(!inventory[tokenId], "already tracked");
        inventory[tokenId] = true;
        if (inventoryCount == 0) {
            oldestTokenId = tokenId;
            newestTokenId = tokenId;
        } else {
            _nextTokenId[newestTokenId] = _encodeTokenId(tokenId);
            _prevTokenId[tokenId] = _encodeTokenId(newestTokenId);
            newestTokenId = tokenId;
        }
        inventoryCount += 1;
    }

    /// @dev Unlinks tokenId from the doubly-linked inventory list and clears its tracking state.
    function _removeInventory(uint256 tokenId) internal {
        if (!inventory[tokenId]) revert NotInInventory();

        uint256 prevEncoded = _prevTokenId[tokenId];
        uint256 nextEncoded = _nextTokenId[tokenId];
        bool hasPrev = prevEncoded != 0;
        bool hasNext = nextEncoded != 0;
        uint256 prevId = hasPrev ? _decodeTokenId(prevEncoded) : 0;
        uint256 nextId = hasNext ? _decodeTokenId(nextEncoded) : 0;

        if (!hasPrev) {
            oldestTokenId = nextId;
        } else {
            _nextTokenId[prevId] = hasNext ? _encodeTokenId(nextId) : 0;
        }

        if (!hasNext) {
            newestTokenId = prevId;
        } else {
            _prevTokenId[nextId] = hasPrev ? _encodeTokenId(prevId) : 0;
        }

        delete _prevTokenId[tokenId];
        delete _nextTokenId[tokenId];
        delete inventory[tokenId];
        inventoryCount -= 1;

        if (inventoryCount == 0) {
            oldestTokenId = 0;
            newestTokenId = 0;
        }
    }

    function _encodeTokenId(uint256 tokenId) private pure returns (uint256) {
        return tokenId + 1;
    }

    /// @dev Reverses _encodeTokenId: (encoded - 1) recovers the original tokenId.
    function _decodeTokenId(uint256 encodedTokenId) private pure returns (uint256) {
        return encodedTokenId - 1;
    }
}
Chain explorer5930msChain node88ms