EquifoldStaking

0xd4c553ba69a07789cc7f262d0bf158511e11f659

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
9,019 bytes
ABI entries
75
22 read · 15 write
License
none

Contract information

Address
0xd4c553ba69a07789cc7f262d0bf158511e11f659
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x323bada6e7…4b9b04602d

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (22)

DEFAULT_ADMIN_ROLE()bytes32
MAXIMUM_DURATION()uint48
PAUSER_ROLE()bytes32
RESERVATION_MANAGER_ROLE()bytes32
STAKING_ADMIN_ROLE()bytes32
availableStake(address)uint256
getRoleAdmin(bytes32)bytes32
hasRole(bytes32, address)bool
minimumLockDuration()uint48
paused()bool
poolCreator(address)address
poolReservedAmount(address)uint256
protocolConfig()address
reservationAmount(bytes32)uint256
reservationCreator(bytes32)address
reservedStake(address)uint256
stakeToken()address
stakeUnlockAt(address)uint48
supportsInterface(bytes4)bool
totalStaked(address)uint256
withdrawalCooldown()uint48
withdrawalRequests(address)uint208, uint48

Events (15)

DurationsUpdatedPausedRoleAdminChangedRoleGrantedRoleRevokedStakeDepositedStakeReleasedStakeReservationCreatedStakeReservationReleasedStakeReservedStakeTokenSetStakeWithdrawnUnpausedWithdrawalCancelledWithdrawalRequested

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "admin",
        "type": "address"
      },
      {
        "internalType": "contract IERC20",
        "name": "stakeToken_",
        "type": "address"
      },
      {
        "internalType": "contract IProtocolConfig",
        "name": "protocolConfig_",
        "type": "address"
      },
      {
        "internalType": "uint48",
        "name": "minimumLockDuration_",
        "type": "uint48"
      },
      {
        "internalType": "uint48",
        "name": "withdrawalCooldown_",
        "type": "uint48"
      }
    ],
    "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": "ActiveWithdrawalRequest",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AmountZero",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientAvailableStake",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidDuration",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidReservationAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoWithdrawalRequest",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolAlreadyReserved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolNotReserved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReservationAlreadyExists",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReservationNotFound",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StakeStillLocked",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StakeTokenAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StakeTokenNotSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenTransferMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WithdrawalCooldownActive",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "minimumLockDuration",
        "type": "uint48"
      },
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "withdrawalCooldown",
        "type": "uint48"
      }
    ],
    "name": "DurationsUpdated",
    "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": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalStaked",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "unlockAt",
        "type": "uint48"
      }
    ],
    "name": "StakeDeposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "StakeReleased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "StakeReservationCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "StakeReservationReleased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "StakeReserved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "stakeToken",
        "type": "address"
      }
    ],
    "name": "StakeTokenSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "remainingStake",
        "type": "uint256"
      }
    ],
    "name": "StakeWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "WithdrawalCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "availableAt",
        "type": "uint48"
      }
    ],
    "name": "WithdrawalRequested",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAXIMUM_DURATION",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PAUSER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "RESERVATION_MANAGER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "STAKING_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "availableStake",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelUnstake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executeUnstake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "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": [],
    "name": "minimumLockDuration",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "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": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "name": "poolCreator",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "name": "poolReservedAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolConfig",
    "outputs": [
      {
        "internalType": "contract IProtocolConfig",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "name": "releaseForPool",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      }
    ],
    "name": "releaseStake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "callerConfirmation",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint208",
        "name": "amount",
        "type": "uint208"
      }
    ],
    "name": "requestUnstake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      }
    ],
    "name": "reservationAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      }
    ],
    "name": "reservationCreator",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "reserveForPool",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "reserveStake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "reservedStake",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "revokeRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint48",
        "name": "newMinimumLockDuration",
        "type": "uint48"
      },
      {
        "internalType": "uint48",
        "name": "newWithdrawalCooldown",
        "type": "uint48"
      }
    ],
    "name": "setDurations",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IERC20",
        "name": "stakeToken_",
        "type": "address"
      }
    ],
    "name": "setStakeToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "stake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stakeToken",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "stakeUnlockAt",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "unlockAt",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "totalStaked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdrawalCooldown",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "withdrawalRequests",
    "outputs": [
      {
        "internalType": "uint208",
        "name": "amount",
        "type": "uint208"
      },
      {
        "internalType": "uint48",
        "name": "availableAt",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IProtocolConfig } from "../interfaces/IProtocolConfig.sol";

/// @title Equifold Creator Staking
/// @author Equifold
/// @notice Locks external EQUI balances and reserves stake against active creator pools.
contract EquifoldStaking is AccessControl, Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error ActiveWithdrawalRequest();
    error AmountZero();
    error InsufficientAvailableStake();
    error InvalidAddress();
    error InvalidDuration();
    error InvalidReservationAmount();
    error NoWithdrawalRequest();
    error PoolAlreadyReserved();
    error PoolNotReserved();
    error ReservationAlreadyExists();
    error ReservationNotFound();
    error StakeStillLocked();
    error StakeTokenAlreadySet();
    error StakeTokenNotSet();
    error TokenTransferMismatch();
    error WithdrawalCooldownActive();

    uint48 public constant MAXIMUM_DURATION = 365 days;

    bytes32 public constant RESERVATION_MANAGER_ROLE = keccak256("RESERVATION_MANAGER_ROLE");
    bytes32 public constant STAKING_ADMIN_ROLE = keccak256("STAKING_ADMIN_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    struct WithdrawalRequest {
        uint208 amount;
        uint48 availableAt;
    }

    // Not immutable: EQUI is fair-launched on the platform itself, so its
    // address doesn't exist when this contract is deployed. It's set exactly
    // once, post-launch, via setStakeToken() and then permanently locked. When
    // a real EQUI is known at deploy time (e.g. tests, testnet mocks), the
    // constructor sets it directly and setStakeToken can never be called.
    IERC20 public stakeToken;
    IProtocolConfig public immutable protocolConfig;

    uint48 public minimumLockDuration;
    uint48 public withdrawalCooldown;

    mapping(address creator => uint256 amount) public totalStaked;
    mapping(address creator => uint256 amount) public reservedStake;
    mapping(address creator => uint48 unlockAt) public stakeUnlockAt;
    mapping(address creator => WithdrawalRequest request) public withdrawalRequests;
    mapping(address pool => address creator) public poolCreator;
    mapping(address pool => uint256 amount) public poolReservedAmount;
    mapping(bytes32 reservationKey => address creator) public reservationCreator;
    mapping(bytes32 reservationKey => uint256 amount) public reservationAmount;

    event StakeDeposited(
        address indexed creator, uint256 amount, uint256 totalStaked, uint48 unlockAt
    );
    event WithdrawalRequested(address indexed creator, uint256 amount, uint48 availableAt);
    event WithdrawalCancelled(address indexed creator, uint256 amount);
    event StakeWithdrawn(address indexed creator, uint256 amount, uint256 remainingStake);
    event StakeReserved(address indexed creator, address indexed pool, uint256 amount);
    event StakeReleased(address indexed creator, address indexed pool, uint256 amount);
    event StakeReservationCreated(
        bytes32 indexed reservationKey, address indexed creator, uint256 amount
    );
    event StakeReservationReleased(
        bytes32 indexed reservationKey, address indexed creator, uint256 amount
    );
    event DurationsUpdated(uint48 minimumLockDuration, uint48 withdrawalCooldown);
    event StakeTokenSet(address indexed stakeToken);

    constructor(
        address admin,
        IERC20 stakeToken_,
        IProtocolConfig protocolConfig_,
        uint48 minimumLockDuration_,
        uint48 withdrawalCooldown_
    ) {
        // stakeToken_ MAY be the zero address here — it is then supplied once,
        // post-launch, via setStakeToken(). protocolConfig/admin are still
        // mandatory. A non-zero stakeToken_ must be a real contract.
        if (admin == address(0) || address(protocolConfig_) == address(0)) {
            revert InvalidAddress();
        }
        if (address(stakeToken_) != address(0) && address(stakeToken_).code.length == 0) {
            revert InvalidAddress();
        }
        _validateDurations(minimumLockDuration_, withdrawalCooldown_);

        stakeToken = stakeToken_;
        protocolConfig = protocolConfig_;
        minimumLockDuration = minimumLockDuration_;
        withdrawalCooldown = withdrawalCooldown_;

        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(STAKING_ADMIN_ROLE, admin);
        _grantRole(PAUSER_ROLE, admin);
    }

    /// @notice Stakes additional EQUI and refreshes the minimum lock timestamp.
    function stake(
        uint256 amount
    ) external whenNotPaused nonReentrant {
        if (amount == 0) revert AmountZero();
        if (address(stakeToken) == address(0)) revert StakeTokenNotSet();
        uint256 balanceBefore = stakeToken.balanceOf(address(this));
        stakeToken.safeTransferFrom(msg.sender, address(this), amount);
        if (stakeToken.balanceOf(address(this)) - balanceBefore != amount) {
            revert TokenTransferMismatch();
        }

        totalStaked[msg.sender] += amount;
        uint48 newUnlockAt = uint48(block.timestamp) + minimumLockDuration;
        if (newUnlockAt > stakeUnlockAt[msg.sender]) stakeUnlockAt[msg.sender] = newUnlockAt;
        emit StakeDeposited(msg.sender, amount, totalStaked[msg.sender], stakeUnlockAt[msg.sender]);
    }

    /// @notice Starts the cooldown for an available, unreserved stake amount.
    /// @dev Deliberately not pausable: a stake withdrawal must never be frozen
    /// by the pause guardian.
    function requestUnstake(
        uint208 amount
    ) external {
        if (amount == 0) revert AmountZero();
        if (block.timestamp < stakeUnlockAt[msg.sender]) revert StakeStillLocked();
        if (withdrawalRequests[msg.sender].amount != 0) revert ActiveWithdrawalRequest();
        if (amount > availableStake(msg.sender)) revert InsufficientAvailableStake();

        uint48 availableAt = uint48(block.timestamp) + withdrawalCooldown;
        withdrawalRequests[msg.sender] =
            WithdrawalRequest({ amount: amount, availableAt: availableAt });
        emit WithdrawalRequested(msg.sender, amount, availableAt);
    }

    /// @notice Cancels an outstanding withdrawal request without moving stake.
    function cancelUnstake() external {
        WithdrawalRequest memory request = withdrawalRequests[msg.sender];
        if (request.amount == 0) revert NoWithdrawalRequest();
        delete withdrawalRequests[msg.sender];
        emit WithdrawalCancelled(msg.sender, request.amount);
    }

    /// @notice Withdraws EQUI after the request cooldown has elapsed.
    /// @dev Deliberately not pausable: a stake withdrawal must never be frozen
    /// by the pause guardian.
    function executeUnstake() external nonReentrant {
        WithdrawalRequest memory request = withdrawalRequests[msg.sender];
        if (request.amount == 0) revert NoWithdrawalRequest();
        if (block.timestamp < request.availableAt) revert WithdrawalCooldownActive();
        if (request.amount > totalStaked[msg.sender] - reservedStake[msg.sender]) {
            revert InsufficientAvailableStake();
        }

        delete withdrawalRequests[msg.sender];
        totalStaked[msg.sender] -= request.amount;
        stakeToken.safeTransfer(msg.sender, request.amount);
        emit StakeWithdrawn(msg.sender, request.amount, totalStaked[msg.sender]);
    }

    /// @notice Reserves creator stake against a newly created active pool.
    function reserveForPool(
        address creator,
        address pool,
        uint256 amount
    ) external onlyRole(RESERVATION_MANAGER_ROLE) whenNotPaused {
        if (creator == address(0) || pool == address(0)) revert InvalidAddress();
        if (poolCreator[pool] != address(0)) revert PoolAlreadyReserved();
        if (amount < protocolConfig.minimumCreatorStake()) revert InvalidReservationAmount();
        if (amount > availableStake(creator)) revert InsufficientAvailableStake();

        poolCreator[pool] = creator;
        poolReservedAmount[pool] = amount;
        reservedStake[creator] += amount;
        emit StakeReserved(creator, pool, amount);
    }

    /// @notice Releases stake after the associated pool has reached a terminal releasable state.
    function releaseForPool(
        address pool
    ) external onlyRole(RESERVATION_MANAGER_ROLE) {
        address creator = poolCreator[pool];
        if (creator == address(0)) revert PoolNotReserved();
        uint256 amount = poolReservedAmount[pool];

        delete poolCreator[pool];
        delete poolReservedAmount[pool];
        reservedStake[creator] -= amount;
        emit StakeReleased(creator, pool, amount);
    }

    /// @notice Reserves stake for any protocol product using a domain-separated key.
    function reserveStake(
        address creator,
        bytes32 reservationKey,
        uint256 amount
    ) external onlyRole(RESERVATION_MANAGER_ROLE) whenNotPaused {
        if (creator == address(0) || reservationKey == bytes32(0)) revert InvalidAddress();
        if (reservationCreator[reservationKey] != address(0)) revert ReservationAlreadyExists();
        if (amount < protocolConfig.minimumCreatorStake()) revert InvalidReservationAmount();
        if (amount > availableStake(creator)) revert InsufficientAvailableStake();

        reservationCreator[reservationKey] = creator;
        reservationAmount[reservationKey] = amount;
        reservedStake[creator] += amount;
        emit StakeReservationCreated(reservationKey, creator, amount);
    }

    /// @notice Releases a product-neutral stake reservation after its lifecycle completes.
    function releaseStake(
        bytes32 reservationKey
    ) external onlyRole(RESERVATION_MANAGER_ROLE) {
        address creator = reservationCreator[reservationKey];
        if (creator == address(0)) revert ReservationNotFound();
        uint256 amount = reservationAmount[reservationKey];

        delete reservationCreator[reservationKey];
        delete reservationAmount[reservationKey];
        reservedStake[creator] -= amount;
        emit StakeReservationReleased(reservationKey, creator, amount);
    }

    /// @notice Returns stake not reserved for pools or committed to a pending withdrawal.
    function availableStake(
        address creator
    ) public view returns (uint256) {
        return totalStaked[creator] - reservedStake[creator] - withdrawalRequests[creator].amount;
    }

    /// @notice Sets the EQUI stake token exactly once, then locks it forever.
    /// @dev Enables the deploy-platform-then-fair-launch-EQUI ordering: the
    /// staking contract is deployed before EQUI exists (stakeToken == 0), then
    /// pointed at the launched EQUI here. Reverts once set, so the token can
    /// never be swapped out from under existing stakers.
    function setStakeToken(
        IERC20 stakeToken_
    ) external onlyRole(STAKING_ADMIN_ROLE) {
        if (address(stakeToken) != address(0)) revert StakeTokenAlreadySet();
        if (address(stakeToken_) == address(0) || address(stakeToken_).code.length == 0) {
            revert InvalidAddress();
        }
        stakeToken = stakeToken_;
        emit StakeTokenSet(address(stakeToken_));
    }

    /// @notice Updates durations for future stakes and withdrawal requests.
    function setDurations(
        uint48 newMinimumLockDuration,
        uint48 newWithdrawalCooldown
    ) external onlyRole(STAKING_ADMIN_ROLE) {
        _validateDurations(newMinimumLockDuration, newWithdrawalCooldown);
        minimumLockDuration = newMinimumLockDuration;
        withdrawalCooldown = newWithdrawalCooldown;
        emit DurationsUpdated(newMinimumLockDuration, newWithdrawalCooldown);
    }

    /// @notice Pauses new staking and reservations. Withdrawals stay available
    /// (requestUnstake/executeUnstake) so staked funds can never be frozen.
    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    /// @notice Restores staking and reservation entry points.
    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function _validateDurations(
        uint48 lockDuration,
        uint48 cooldown
    ) private pure {
        if (lockDuration > MAXIMUM_DURATION || cooldown == 0 || cooldown > MAXIMUM_DURATION) {
            revert InvalidDuration();
        }
    }
}
Chain explorer3228msChain node81ms