Smart Contracts

CarbStaking

Tier staking for CARB with a warm-up and a cooldown. The tier is read by the marketplace as a trading-fee discount. No rewards of any kind.

Inherits: AccessControl, ReentrancyGuard, IFeeDiscount. Source: contracts/token/CarbStaking.sol.

Legal notice

Technical reference for unaudited contracts. Not investment advice, not an offer or solicitation, no returns promised. CARB may be restricted in your jurisdiction and is not launched yet. Get your own advice.

Stake CARB to unlock an access tier 0 to 3. The marketplace reads the tier as a discount on its trading fee. Staking pays nothing: there is no reward token, emission, yield or lock-up bonus, and staked tokens are never lent or touched by any admin.

Constructor

constructor(
    address carb_,
    address admin,
    uint256 warmup,
    uint256 cooldown,
    uint256[3] memory thresholds,
    uint16[4] memory discountsBps
)
RevertCondition
Invalid tokencarb_ is the zero address
Invalid adminadmin is the zero address
Warmup out of boundswarmup outside MIN_WARMUP..MAX_WARMUP
Cooldown out of boundscooldown outside MIN_COOLDOWN..MAX_COOLDOWN
Threshold too low, Thresholds not increasing, Threshold too highinvalid thresholds (see below)
Tier 0 has no discount, Discounts decreasing, Discount too highinvalid discountsBps (see below)

admin receives DEFAULT_ADMIN_ROLE and PARAMETER_ROLE.

Immutable and constant parameters

NameValueNotes
carbconstructor argimmutable
WARMUPconstructor argimmutable. Bounds: MIN_WARMUP = 1 hour, MAX_WARMUP = 14 days
COOLDOWNconstructor argimmutable. Bounds: MIN_COOLDOWN = 1 day, MAX_COOLDOWN = 30 days
TIER_COUNT4tiers 0 to 3
MIN_THRESHOLD1 CARBsmallest tier-1 threshold
MAX_THRESHOLD50,000,000 CARBlargest tier-3 threshold
MAX_DISCOUNT_BPS500050 % of the fee; the marketplace clamps to the same value independently

Tier parameters

NameRulesChangeable by
tierThresholds[3] (tier 1, 2, 3)1 CARB <= t1 < t2 < t3 <= 50,000,000 CARBPARAMETER_ROLE
tierDiscountBps[4] (tier 0 to 3)tier 0 is 0, non-decreasing, last entry <= 5000PARAMETER_ROLE

Changes take effect immediately for every account.

Default in deployment helper, owner may change before launch

The deployment helper (scripts/lib/governance-setup.js and the tests) uses thresholds of 1,000 / 5,000 / 25,000 CARB, discounts of 0 / 10 / 25 / 50 % of the fee, a warm-up of 3 days and a cooldown of 7 days. These are launch parameters chosen by the owner and may change before launch. Published values will be listed at launch (contracts/contracts/docs/TOKEN.md, parameters table).

Position

struct Position {
    uint128 matured;      // staked and warmed up: counts for the tier
    uint128 pending;      // staked, still warming up
    uint64  pendingSince; // timestamp of the latest stake into `pending`
    uint128 cooling;      // unstaked, waiting for the cooldown
    uint64  unlockAt;     // when `cooling` becomes withdrawable
}

totalStaked is the sum of pending and matured stake of all users and excludes tokens in cooldown.

How warm-up and cooldown work

  • stake puts CARB into pending. Only matured stake (pending that has aged at least WARMUP) counts for tierOf. Borrowing CARB, staking and reading the tier inside one transaction or block therefore gives no uplift: the tier stays that of the previously matured stake.
  • A top-up restarts the age of the pending bucket only. Already-matured stake keeps its tier.
  • unstake takes effect on the tier immediately: pending is consumed first, then matured. It starts the cooldown, and a further unstake restarts the cooldown for the whole cooling amount. Tokens in cooldown count for nothing.
  • withdraw releases all tokens whose cooldown has elapsed.
  • A tier therefore requires capital that stays committed for at least WARMUP going in and COOLDOWN going out.

Functions

FunctionAccessBehavior
stake(amount)anyone, nonReentrantPulls amount CARB (needs a prior approve to this contract) into pending. The app uses approve-then-stake, no permit. Emits Staked.
unstake(amount)anyone, nonReentrantMoves amount from pending/matured into cooling and sets unlockAt = now + COOLDOWN. Emits Unstaked(account, amount, unlockAt).
withdraw() returns amountanyone, nonReentrantTransfers the whole cooling balance after unlockAt. Emits Withdrawn.
effectiveStake(account)viewmatured stake, plus pending stake that has finished warming up
tierOf(account)viewtier 0 to 3 from effectiveStake and the current thresholds
discountBps(user)view (IFeeDiscount)tierDiscountBps[tierOf(user)], in bps of the fee
positionOf(account)view(matured, pending, pendingSince, cooling, unlockAt) as stored
balanceOf(account)viewmatured + pending + cooling
setTierThresholds(uint256[3])PARAMETER_ROLEReplaces the thresholds. Emits TierThresholdsUpdated.
setTierDiscounts(uint16[4])PARAMETER_ROLEReplaces the discounts. Emits TierDiscountsUpdated.

Public getters: carb, WARMUP, COOLDOWN, tierThresholds(i), tierDiscountBps(i), totalStaked, and the constants above. effectiveStake and tierOf are evaluated at the current block time, so an account's tier can rise without any transaction once its pending stake has aged.

The fee discount

CarbStaking is the contract the marketplace is pointed at with CarbonMarketplace.setFeeDiscountProvider. The discount applies to the trading fee only, never to the price, is capped at 50 % of the fee, and applies on the CCT seller side of order-book and auction sales. AMM swaps are not discounted. Details: CarbonMarketplace.

Events

EventParameters
Stakedaddress indexed account, uint256 amount
Unstakedaddress indexed account, uint256 amount, uint256 unlockAt
Withdrawnaddress indexed account, uint256 amount
TierThresholdsUpdateduint256[3] thresholds
TierDiscountsUpdateduint16[4] discountsBps

Errors (revert strings)

MessageFunctionCondition
Invalid amountstake, unstakeamount == 0
Amount too largestakeamount above uint128 max
Insufficient stakeunstakeamount above matured + pending
Nothing to withdrawwithdrawno cooling balance
Cooldown activewithdrawblock.timestamp < unlockAt
Threshold too lowthresholdst1 < MIN_THRESHOLD
Thresholds not increasingthresholdsnot t1 < t2 < t3
Threshold too highthresholdst3 > MAX_THRESHOLD
Tier 0 has no discountdiscountsfirst entry is not 0
Discounts decreasingdiscountsan entry is lower than the previous one
Discount too highdiscountslast entry above MAX_DISCOUNT_BPS

Role failures revert with the OpenZeppelin AccessControl: account ... is missing role ... message.

Roles

RoleHolderGates
DEFAULT_ADMIN_ROLEadmin (multisig)role management
PARAMETER_ROLEadmin at construction; the governance timelock from stage 1 of the decentralisation plansetTierThresholds, setTierDiscounts

No role can move, freeze, seize or redirect staked tokens, and there is no rescue function.

Caveats

  • Staked tokens carry no votes (this contract delegates nowhere). A user choosing a tier gives up that voting power.
  • The discount follows the account's current tier, per address. Splitting stake across addresses does not add up, and a seller who unstakes loses the discount on fills that happen afterwards.
  • Nothing is paid to stakers. The only effect of a tier is the lower marketplace fee.
  • Not upgradeable.