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
- Creator
- 0xd03543d97B…F446Cd1418
- 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