Skip to content
Contract reference View as Markdown

SqueekStakingLink to this section

TL;DRLink to this section

  • SqueekStaking holds deposited Squeeks and staked $SQK, updates account power in the same transaction as a deposit or stake, and streams $SQK funded by SqueekMining over 1,200 seconds.
  • Account power is deposited Squeek power P plus boosted $SQK power Q; every change settles rewards at the old power first.
  • Rewards accrue to the address that staked, not to a Squeek: claim them with claim(account) or compound them with claimAndStake().
  • Unstaking burns a bounded exit fee; withdrawals, unstaking and claims stay available while intake is paused.

OverviewLink to this section

In active development

These contracts may change before mainnet. Pin the ABI you use and re-check the deployment before relying on it.

The contract holds deposited Squeeks and staked $SQK principal and streams rewards. All $SQK issuance reaches it through fund, the only call SqueekMining makes into it. It reads SqueekCore for the ten-minute round clock and Guardian, and the Genesis collection (SqueekNFT) for levels and ownership.

Power is in WAD (10^18 per whole unit). A deposited Squeek adds its level's base power; staked $SQK adds Q, which grows with the account's Squeek power. Power takes effect in the same transaction, and rewards earned under the previous power stay with the account.

The Sepolia proxy address and identities are under Deployments.

FunctionsLink to this section

TypesLink to this section

struct StakingInitialization {
    address mining;              // sole reward funder
    address token;               // SQK
    address core;                // native clock and guardian source
    address governance;          // the admin Timelock
    address genesis;             // the Genesis collection (SqueekNFT)
    uint64[] genesisBasePower;   // whole power units for levels 1..MAXIMUM_LEVEL
    uint256 sqkPerPower;         // c: SQK base units per whole power unit; 0 < c < 2^96
    uint256 referencePower;      // P_ref: WAD NFT power that doubles the SQK rate; 0 < P_ref < 2^128
    StakingExitPolicy exitPolicy;
}

struct StakingExitPolicy {
    uint16 minBps;          // marginal fee with no recent exits; <= maxBps
    uint16 maxBps;          // marginal fee at and beyond saturation; < 10_000
    uint16 saturationBps;   // share of the pressure denominator that reaches maxBps; 1..10_000
    uint8 windowDays;       // UTC days counted as recent, today included; 1..30
    uint128 pressureFloor;  // smallest pressure denominator, SQK base units; > 0
}

struct StakingRewardState {
    uint256 funded;          // lifetime SQK funded by Mining
    uint256 paid;            // lifetime SQK claimed out
    uint256 compounded;      // lifetime SQK restaked by claimAndStake
    uint256 rewardIndex;     // Q128 reward per WAD of power
    uint256 remaining;       // funded SQK not yet released
    uint128 totalNftPower;   // sum of P, WAD
    uint128 totalSqkPower;   // sum of Q, WAD
    uint256 streamRate;      // informational: stream amount / 1,200, per second
    uint64 lastAccruedAt;    // the read timestamp (views project to now)
    uint64 streamEndsAt;     // projected end if power stays nonzero; 0 when exhausted
}

uint8 and uint16 fields decode to number in viem; every other numeric field decodes to bigint. The policy values, base-power table, c and P_ref are deployment parameters fixed at initialization; read them with exitPolicy(), basePower and powerParameters().

Squeek custodyLink to this section

depositLink to this section

function deposit(address collection, uint256[] calldata tokenIds) external;

Pulls 1–100 of the caller's own Squeeks into custody and raises P by each token's base power at its current level. Tokens are pulled from msg.sender, so an operator can't deposit someone else's; approve Staking on the collection first.

Name Type Unit Meaning
collection address — The Genesis collection (genesis())
tokenIds uint256[] — 1–100 token IDs owned by the caller

Reverts with InvalidDeposit (empty or over 100, unbound collection, or the caller is the collection's sale custodian), InvalidCollection, EmergencyPaused, InvalidLevel, CustodyMismatch, InvalidBounds or ReentrantCall.

onERC721ReceivedLink to this section

function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4);

A safeTransferFrom to Staking from a bound collection with empty data deposits the token for from, the previous owner. Mints (from zero), non-empty data, and transfers from the sale custodian (the collection's launch, which holds IDs 1–8,888 until it transfers them) are rejected with InvalidDeposit; a Squeek deposits from its owner's wallet. Also reverts with InvalidCollection, EmergencyPaused, CustodyMismatch, InvalidLevel, InvalidBounds or ReentrantCall.

withdrawLink to this section

function withdraw(address collection, uint256[] calldata tokenIds) external;

Returns 1–100 of the caller's deposited Squeeks and lowers P (and with it Q); earned rewards and staked $SQK are untouched. Works while intake is paused. Reverts with InvalidWithdrawal (empty or over 100), Unauthorized (the caller is not the token's depositor), InvalidLevel, InvalidBounds or ReentrantCall.

mergeDepositedLink to this section

function mergeDeposited(address collection, uint256 survivor, uint256 donor) external;

Merges two of the caller's deposited Squeeks under the collection's merge rules (same level, cooldown, merge gate). The donor is burned; the survivor stays deposited one level higher and P rises by basePower(L + 1) − 2 × basePower(L), always positive. Levels are checked before and after, so a misreporting collection can't misstate power. Reverts with Unauthorized, EmergencyPaused, InvalidLevel, InvalidBounds, ReentrantCall or the collection's merge error.

recoverLink to this section

function recover(address collection, uint256 tokenId, address to) external;

The admin only. Moves out a token Staking holds that has no registered depositor (for example one sent with plain transferFrom). Works while paused. Reverts with Unauthorized, RegisteredToken or ReentrantCall.

$SQK stakingLink to this section

stakeLink to this section

function stake(uint256 amount) external;

Pulls amount $SQK from the caller (approve Staking on $SQK first) and requires the balance to rise by exactly that. Power updates in the same transaction; the amount is fresh principal and can be unstaked from the next round (see Freshness). Reverts with InvalidStake (zero, or a balance change other than amount), EmergencyPaused, InvalidBounds, SafeERC20FailedOperation, CustodyShortfall or ReentrantCall.

claimAndStakeLink to this section

function claimAndStake() external returns (uint256 amount);

Settles the caller and restakes all whole claimable $SQK as fresh principal, without moving tokens. Returns the amount compounded, 0 if none. Reverts with EmergencyPaused, ExclusiveClaimRecipient (the caller has set a claim recipient), InvalidBounds, CustodyShortfall or ReentrantCall.

unstakeLink to this section

function unstake(uint256 amount, uint256 maxFee, uint64 deadline) external returns (uint256 net);

Removes amount of the caller's mature principal, burns the exit fee from Staking's balance and pays net = amount − fee to the caller. Power falls in the same transaction. Works while intake or Mining is paused.

Name Type Unit Meaning
amount uint256 $SQK base units Gross principal to remove; at most mature principal
maxFee uint256 $SQK base units Largest fee accepted; quote with quoteUnstake
deadline uint64 Unix seconds Last timestamp the call is accepted

Reverts with DeadlineExpired, InvalidUnstake (zero, or more than mature principal), FeeLimitExceeded(fee, maxFee), InvalidBounds, SafeERC20FailedOperation, SafeCastOverflowedUintDowncast, CustodyShortfall or ReentrantCall.

RewardsLink to this section

fundLink to this section

function fund(uint256 amount) external;

SqueekMining only, after transferring amount $SQK. Checks the balance covers it, accrues the old stream, then restarts a 1,200-second stream over the unreleased remainder plus amount; $SQK sent any other way is never allocated. Reverts with Unauthorized, InvalidFunding (zero), CustodyShortfall or ReentrantCall.

claimLink to this section

function claim(address account) external returns (uint256 amount);

Pays all whole claimable $SQK of account to its claim recipient (by default the account), which must be the caller. Returns 0 if nothing is claimable; sub-unit dust carries over. Works while intake or Mining is paused. Reverts with Unauthorized, SafeERC20FailedOperation, CustodyShortfall or ReentrantCall.

setClaimRecipientLink to this section

function setClaimRecipient(address recipient) external;

Sets the caller's claim recipient. Only that address can call claim(caller), and claimAndStake reverts for the caller until it clears the recipient. Zero or the caller's own address clears it. Reverts with InvalidClaimRecipient if recipient is Staking, or ReentrantCall.

Intake pauseLink to this section

pause, unpause, pausedLink to this section

function pause() external;
function unpause() external;
function paused() external view returns (bool);

The current Core guardian pauses intake; only the admin resumes it. A pause stops deposit, onERC721Received, stake, claimAndStake and mergeDeposited; everything else, including accrual, fund, claim, withdraw and unstake, keeps working. pause and unpause revert with Unauthorized or ReentrantCall.

ViewsLink to this section

accountPowerLink to this section

function accountPower(address account) external view returns (uint256 nftPower, uint256 sqkPower);

The account's current P and Q in WAD. Its power is P + Q.

stakeOfLink to this section

function stakeOf(address account) external view returns (uint256 principal, uint256 freshPrincipal, uint64 freshUntilRound);

Staked principal, the part still fresh, and the round in which it matures. freshPrincipal reads 0 once that round has started; mature principal is principal − freshPrincipal.

totalStaked, totalFreshLink to this section

function totalStaked() external view returns (uint256);
function totalFresh() external view returns (uint256 freshPrincipal, uint64 freshUntilRound);

All staked principal, and the fresh part of it with its maturity round. Principal is separate from unpaid rewards and donations.

quotePower, powerParametersLink to this section

function quotePower(uint256 principal, uint256 nftPower) external view returns (uint256);
function powerParameters() external view returns (uint256 sqkPerPower, uint256 referencePower);

Q for a principal and NFT power under the fixed formula, and its parameters c and P_ref. quotePower reverts with InvalidBounds when P or Q would reach 2^128.

quoteUnstakeLink to this section

function quoteUnstake(address account, uint256 amount) external view returns (uint256 fee, uint256 net);

The fee and net unstake(amount, …) would produce now for account. Reverts with InvalidUnstake for zero or more than mature principal. Exits by others in the same window raise the fee, so pass the quote as maxFee with a short deadline.

exitPolicy, recentExitsLink to this section

function exitPolicy() external view returns (StakingExitPolicy memory);
function recentExits() external view returns (uint256);

The fixed exit-fee constants, and gross $SQK unstaked in the current UTC day and the windowDays − 1 days before it.

claimableLink to this section

function claimable(address account) external view returns (uint256);

Whole $SQK the account can claim now, projected to the current timestamp.

claimRecipient, rewardStateLink to this section

function claimRecipient(address account) external view returns (address);
function rewardState() external view returns (StakingRewardState memory);

The address allowed to claim the account's rewards (the account unless one is set), and the projected stream and lifetime totals.

depositor, basePower, saleCustodian, launchReadyLink to this section

function depositor(address collection, uint256 tokenId) external view returns (address);
function basePower(address collection, uint8 level) external view returns (uint64);
function saleCustodian(address collection) external view returns (address);
function launchReady() external view returns (bool);

The account a token is deposited for (zero if none); whole power units per level (zero outside the table); the sale custodian, the collection's launch, whose transfers cannot deposit; and whether the Genesis collection has issued its full MAX_SUPPLY (lifetime issuance, so later merge burns do not undo it).

BindingsLink to this section

function mining() external view returns (address);
function token() external view returns (address);
function core() external view returns (address);
function governance() external view returns (address);
function genesis() external view returns (address);

The sole funder, $SQK, Core, the admin Timelock (governance()) and the Genesis collection.

Constants and identityLink to this section

function MAX_TOKENS() external view returns (uint256);
function POWER_UNIT() external view returns (uint256);
function implementationCodeAddress() external view returns (address);
function implementationStorageLayoutDigest() external pure returns (bytes32);

MAX_TOKENS is 100 tokens per deposit or withdrawal and POWER_UNIT is 10^18. The identity getters return the running implementation's address and storage-layout digest; state lives in its own ERC-7201 storage namespace. The contract runs behind a guarded transparent proxy that rejects an initializer changing the implementation and exposes deploymentIdentity(); upgrades go through its ProxyAdmin, owned by the admin Timelock.

Admin and setupLink to this section

initializeLink to this section

function initialize(StakingInitialization calldata init) external;

Runs once, during proxy construction only. Checks that Mining, $SQK, Core and the admin Timelock are bound to each other, that Core's round is 600 seconds, that the Genesis collection is the reviewed runtime sharing Core and the admin Timelock, and that each base-power level is positive and more than double the one below. Reverts with InvalidInitialization, NotInitializing, InvalidProxyContext, InvalidMining, InvalidBinding, InvalidGovernance, InvalidCollection, InvalidBasePower, BindingCallFailed, InvalidBounds, InvalidExitPolicy or SafeCastOverflowedUintDowncast.

EventsLink to this section

event CollectionBound(address indexed collection, uint64[] basePower);
event IntakePaused(address indexed guardian);
event IntakeResumed();
event Deposited(address indexed account, address indexed collection, uint256 indexed tokenId, uint8 level, uint64 basePower, uint256 power);
event Withdrawn(address indexed account, address indexed collection, uint256 indexed tokenId, uint8 level, uint64 basePower, uint256 power);
event Recovered(address indexed collection, uint256 indexed tokenId, address indexed to);
event Staked(address indexed account, uint256 amount, uint256 principal, uint64 freshUntilRound);
event DepositedMerge(address indexed account, address indexed collection, uint256 indexed survivor, uint256 donor, uint8 level, uint256 powerDelta);
event Unstaked(address indexed account, uint256 amount, uint256 fee, uint256 net);
event ClaimedAndStaked(address indexed account, uint256 amount, uint256 principal, uint64 freshUntilRound);
event PowerChanged(address indexed account, uint256 nftPower, uint256 sqkPower);
event RewardFunded(uint256 amount, uint256 remaining, uint64 streamEndsAt);
event RewardClaimed(address indexed account, address indexed recipient, uint256 amount);
event ClaimRecipientSet(address indexed account, address indexed recipient);
event Initialized(uint64 version);
Event Emitted when
CollectionBound Initialization binds the Genesis collection and its base-power table
Initialized Initialization
IntakePaused, IntakeResumed The guardian pauses or the admin resumes intake
Deposited, Withdrawn Once per token; power is basePower × 10^18
DepositedMerge A deposited merge; level is the survivor's new level
Recovered The admin recovers an unregistered token
Staked, ClaimedAndStaked Principal added; principal is the new total and freshUntilRound its maturity round
Unstaked Principal removed; fee was burned and net paid
PowerChanged After every custody, stake, merge or unstake, with the new P and Q
RewardFunded Mining funds; remaining is the restarted stream amount
RewardClaimed A claim pays recipient
ClaimRecipientSet An account sets or clears (zero) its claim recipient

ErrorsLink to this section

Error Cause Fix
Unauthorized Wrong caller: not the depositor, claim recipient, guardian, admin or Mining Call from the authorized address
ReentrantCall Re-entered a state-changing entry point Make one call at a time
EmergencyPaused Intake action while paused Wait for the admin to resume; exits still work
InvalidDeposit Empty or over-100 batch, unbound collection, mint, non-empty callback data, or sale custodian as depositor Deposit 1–100 tokens from the owner's wallet
InvalidWithdrawal Empty or over-100 batch Withdraw 1–100 tokens
InvalidLevel(collection, tokenId, level) A level outside the table, or a merge that did not raise the survivor exactly one level from a same-level donor Check levels with the collection
CustodyMismatch(collection, tokenId) The collection does not report Staking as owner after a transfer Deposit through the bound collection
RegisteredToken(collection, tokenId) Recovery of a deposited token Only the depositor can withdraw it
InvalidStake Zero stake, or $SQK moved a different amount Stake a positive amount
InvalidUnstake Zero, or more than mature principal Read stakeOf; wait for fresh principal to mature
DeadlineExpired block.timestamp > deadline Resubmit with a later deadline
FeeLimitExceeded(fee, maxFee) The fee rose above maxFee Re-quote with quoteUnstake
InvalidFunding fund(0) Mining funds positive amounts
CustodyShortfall(balance, required) $SQK balance below principal plus unpaid, uncompounded funding The call rolled back; investigate
InvalidBounds Principal, power or total power would reach 2^128, or invalid c/P_ref Use a smaller amount
InvalidClaimRecipient Recipient set to Staking Choose another address
ExclusiveClaimRecipient claimAndStake with a claim recipient set Clear the claim recipient, or claim through it
InvalidProxyContext, InvalidMining, InvalidBinding, InvalidGovernance, InvalidCollection, InvalidBasePower, InvalidExitPolicy, BindingCallFailed Invalid bindings, tables or policy, or a collection that no longer reports its sale custodian Setup only
InvalidInitialization, NotInitializing Initialization outside proxy construction Setup only
CoreUnavailable, CoreInconsistent A SqueekCore read failed or returned a malformed answer The call rolled back; investigate SqueekCore
InvalidCollectionRuntime At initialization, the Genesis collection is not the reviewed SqueekNFT runtime Setup only
SafeERC20FailedOperation(token) $SQK transfer failed Check balance and allowance
SafeCastOverflowedUintDowncast(bits, value) Inherited overflow guard The call rolled back; investigate

Integrator patternsLink to this section

  • Approve, then deposit. Call setApprovalForAll(staking, true) on SqueekNFT and deposit(genesis(), ids), or safeTransferFrom(owner, staking, id) with empty data. Never send with plain transferFrom: the token is not credited and only the admin can recover it.
  • Deposit from the owner's wallet. Tokens held by saleCustodian(collection), the collection's launch, cannot deposit; they deposit once transferred to an owner's wallet.
  • Quote before unstaking. Call quoteUnstake(account, amount), pass fee as maxFee and a near deadline.
  • Unstake only mature principal. Mature principal is principal − freshPrincipal from stakeOf; fresh principal matures when round freshUntilRound starts.
  • Read rewards from Staking. claimable(account) is projected to the current timestamp; claim with claim(account) from claimRecipient(account).
  • Track power from events. PowerChanged carries the full new P and Q; total power is rewardState().totalNftPower + totalSqkPower.
  • Don't send $SQK directly. Raw transfers are donations that never become rewards or principal.

Account powerLink to this section

$SQK powerLink to this section

With principal S ($SQK base units), Squeek power P (WAD), c = sqkPerPower and P_ref = referencePower:

Q = floor(S * 10^18 * (P_ref + P) / (c * P_ref))
W = P + Q

At P = 0, c $SQK base units buy one whole power unit; at P = P_ref the rate doubles. Q is recomputed whenever S or P changes, so withdrawing Squeeks lowers it too. Each change settles the account under the old W, then applies the new W in the same transaction. P, Q and total power stay below 2^128.

Freshness and maturityLink to this section

Rounds last ten minutes. For t ≥ genesisTimestamp, the current round is floor((t − genesisTimestamp) / 600) + 1. Staked or compounded $SQK earns from the same transaction but stays fresh until the next round, freshUntilRound = floor((t − genesisTimestamp) / 600) + 2 (round 1 before genesis), and can be unstaked once that round starts. Fresh principal does not count in the exit-fee denominator.

Reward streamLink to this section

Each fund accrues the old stream, adds amount to the unreleased remainder R, and restarts a 1,200-second stream of A = R. After e seconds of stream time, floor(A * e / 1200) has been released. Stream time pauses while total power is zero, leaving the unreleased $SQK in remaining. Released $SQK raises a Q128 index by released * 2^128 / W_total; an account earns W * Δindex / 2^128, with global and per-account remainders kept so no checkpoint pattern changes the result. Earned $SQK stays with the account after it withdraws or unstakes.

Exit feeLink to this section

Let x be recent exits (recentExits()), a the unstaked amount and M all mature principal before the exit. With the policy constants:

D = max(M + x, pressureFloor)
C = max(floor(D * saturationBps / 10000), 1)
rate(y) = minBps + (maxBps − minBps) * (y / C)^2   for y ≤ C
rate(y) = maxBps                                   for y > C
fee = ∫ from x to x + a of rate(y) / 10000 dy

The marginal rate starts at minBps, rises quadratically to maxBps once recent exits reach saturationBps of the denominator, then stays flat; the contract evaluates the integral with floored integer steps. Because maxBps < 10000, the fee never exceeds the amount (rounding can make it equal on a dust exit), and splitting an exit gives the same total. The window is whole UTC days: a day drops out at midnight. The fee is burned.

SolvencyLink to this section

After every state change Staking requires:

SQK balance ≥ totalStaked + funded − paid − compounded

Staked principal never backs reward liabilities, and fund checks the new amount is already held.

Usage exampleLink to this section

Reads an account's power, principal, rewards and an exit quote, using setup.ts and verifyGraph() from the Quickstart to verify the proxy at STAKING_ADDRESS.

// staking-position.ts
import { getAddress, parseAbi } from "viem";
import { blockNumber, client, required } from "./setup";
import { verifyGraph } from "./verify";

const { staking } = await verifyGraph();
const account = getAddress(required("ACCOUNT_ADDRESS"));
const abi = parseAbi([
  "function accountPower(address account) view returns (uint256 nftPower, uint256 sqkPower)",
  "function stakeOf(address account) view returns (uint256 principal, uint256 freshPrincipal, uint64 freshUntilRound)",
  "function claimable(address account) view returns (uint256)",
  "function quoteUnstake(address account, uint256 amount) view returns (uint256 fee, uint256 net)",
]);
const read = { address: staking, abi, blockNumber } as const;
const [nftPower, sqkPower] = await client.readContract({ ...read, functionName: "accountPower", args: [account] });
const [principal, freshPrincipal, freshUntilRound] = await client.readContract({ ...read, functionName: "stakeOf", args: [account] });
const rewards = await client.readContract({ ...read, functionName: "claimable", args: [account] });
const mature = principal - freshPrincipal;
const quote = mature > 0n
  ? await client.readContract({ ...read, functionName: "quoteUnstake", args: [account, mature] })
  : undefined;
console.log({ power: nftPower + sqkPower, principal, mature, freshUntilRound, rewards, quote });

ABILink to this section

SqueekStaking.json matches the Sepolia deployment. Call it on the proxy address listed under Deployments, not the implementation.