Smart Contracts

Smart Contracts

JellyPadStaker

JellyPad's in-house liquid staker. Replaces the v3 dependency on Splash spSEI. The receipt token is jpSEI. The contract IS the ERC-20.

Three pools of value

  1. Buffer (native SEI on this contract's balance, tracked in bufferSei). Pays instant-unstake exits.
  2. Validator stake (delegated principal, off-chain at the Cosmos staking module via the precompile at 0x0000000000000000000000000000000000001005). Earns ~4.4% APR.
  3. Pending slow-unstake debt (totalPendingSlowSei). 21-day Cosmos unbonds queued for users.

Key state

VariablePurpose
bufferSeiNative SEI in contract, immediately spendable for instant exits
totalDelegatedSEI delegated to validators, earning rewards
totalPendingSlowSeiSum of queued 21-day claims
targetBufferOwner-set absolute floor on the target
dynamicModeToggle: when on, target is computed from supply / demand / pending claims
bufferSupplyRatioBps% of total supply to keep buffered (clamped 1% to 30%)
demandMultiplierBpsMultiplier on trailing-7d demand (capped 3x)
feeTiers[]0.5% / 1% / 2% / 4% based on buffer ratio
minBufferRatioBpsBelow this ratio, instant-unstake reverts

stake(bool autoCompound) payable

Mirrors ISpSEIPool.stake(bool) so call sites that used Splash drop in unchanged.

function stake(bool /* autoCompound */) external payable {
    require(msg.value >= MIN_STAKE, "amount too small");
 
    uint256 jpSeiOut = previewStake(msg.value);
    uint256 target   = effectiveTargetBuffer();
 
    if (bufferSei < target) {
        // Fill buffer up to target, delegate the rest.
        ...
    } else {
        _delegate(msg.value);
    }
 
    _mint(recipient, jpSeiOut);
}

The mint amount is computed at the rate snapshot before updating accounting. New stakes are rate-neutral.

instantUnstake(uint256 jpSeiAmount)

Burn jpSEI, pay native SEI from buffer at the current tier fee. The fee stays in the buffer (lifts the rate).

Tiered fee schedule:

Buffer ratioFee
≥ 100% of target0.5%
50% to 100%1.0%
20% to 50%2.0%
5% to 20%4.0%
< 5%reverts (buffer too low; use slow unstake)

unstake(uint256 jpSeiAmount) and claimSlowUnstake()

Slow path. Queues a 21-day Cosmos undelegation. No fee.

function unstake(uint256 jpSeiAmount) external {
    require(pendingSlowSei[msg.sender] == 0, "claim pending unstake first");
    uint256 unbondAmount = previewUnstake(jpSeiAmount);
    require(totalDelegated >= unbondAmount, "not enough delegated");
 
    _burn(msg.sender, jpSeiAmount);
    STAKING.undelegate(activeValidator, _toUsei(unbondAmount));
    totalDelegated -= unbondAmount;
    pendingSlowSei[msg.sender] = unbondAmount;
    pendingSlowReadyAt[msg.sender] = block.timestamp + 21 days;
    totalPendingSlowSei += unbondAmount;
}

claim() (anyone)

Pulls validator rewards from the distribution precompile. Lifts the exchange rate. Permissionless.

Dynamic buffer

When dynamicMode = true, the target is max(static, supplyRatio, pendingSlow, demandMultiplier). See decisions.mdx for the full rationale.

Owner ops

MethodEffect
seedBuffer() payableDeposit SEI to buffer, receive jpSEI back. Recoverable later.
donateToBuffer() payablePermanent gift, no jpSEI minted. Lifts rate for everyone.
harvestSurplus(to, amount)Withdraw above effective target
setTargetBuffer(uint256)Cannot drop below totalPendingSlowSei
setDynamicMode(bool)Toggle dynamic targeting
setDynamicParams(ratio, mult, trigger)All bounded
setActiveValidator(string)Rotate validator
setProtocolFeeSink(addr, allowed)Whitelist factory/router for donateToBuffer
undelegateToBuffer(amount)Queue an owner-initiated 21d unbond
markUndelegationComplete(amount)After 21d, credit returned SEI to buffer (legacy alias for topUpFromUndelegation; same logic, owner-only)
rescueERC20(token, to, amount)Pull stranded ERC-20s. Cannot rescue jpSEI itself (would inflate claims).
rescueNative(to, amount)Pull native SEI that isn't part of bufferSei. Refuses to dip below bufferSei — buffer principal is contractually owed to jpSEI holders and cannot be rescued.

Permissionless ops

MethodEffect
claim()Pull validator rewards from the distribution precompile. Lifts the exchange rate.
rebalance()Queue a 21-day refill from totalDelegated when buffer dips below rebalanceTriggerBps.
topUpFromUndelegation(amount)After a 21d unbond returns SEI to the contract, anyone can credit it to bufferSei + decrement totalDelegated. The dead-team fallback for the user-redemption path.

Aggregate read

bufferHealth() returns (buffer, target, delegated, pendingSlow, supply, rate, ratioBps, tierFeeBps, demand7d, dynamic) in one call. The operator console + public dashboard read this directly so all values are from the same block.

Critical implementation note: wei vs usei

Sei's staking precompile is asymmetric.

  • delegate(string) is payable. The precompile reads msg.value (wei) and converts internally.
  • undelegate(string, uint256) takes the amount as a uint256 parameter, interpreted as usei (Cosmos 1e6 denom), not wei (EVM 1e18).

This bit us on the v1 staker deploy. Mixing units silently reverts. The fix is _toUsei(wei) = wei / 1e12 applied to every undelegate call. See JellyPadStaker.sol.

JellyPadToken

The launched-token contract. ERC-20 + bonding curve + DEX-graduation logic.

Constructor params (InitParams)

FieldTypeNotes
name / symbolstringERC-20 metadata
imageURIstringOff-chain image
descriptionstringFree-text
factoryaddressDeploying factory
spSeiPooladdressThe JellyPadStaker. Same address used for both pool and token in v4
spSeiTokenaddressSame
dexRouteraddressSaphyre/DragonSwap V2 router
graduationMcapUsduint256$69k = 69_000e18 default
seiUsdPriceuint256USD wei per SEI

The fields are still named spSeiPool and spSeiToken for ABI continuity with v3 deployments. They both point at the JellyPadStaker now.

buy() payable

function buy() external payable nonReentrant {
    require(!graduated, "Graduated to DEX");
    require(msg.value > 0, "Send SEI");
 
    uint256 fee     = (msg.value * 100) / 10_000;     // 1%
    uint256 seiIn   = msg.value - fee;
    uint256 tokensOut = _getTokensOut(seiIn);
 
    // Stake principal in-flight via JellyPadStaker.
    SPSEI_POOL.stake{value: seiIn}(false);
 
    realSeiReserves      += seiIn;
    virtualSeiReserves   += seiIn;
    virtualTokenReserves -= tokensOut;
 
    _transfer(address(this), msg.sender, tokensOut);
    payable(factory).call{value: fee}("");
 
    emit Buy(msg.sender, seiIn, tokensOut, _currentPrice());
    _checkGraduation();
}

sell()

Computes the curve's gross SEI output, takes a 1% protocol fee, calls instantUnstake on the staker for enough jpSEI to cover. Uses worst-case 4% tier in the redemption math so sells succeed even when the buffer is stressed; any dust stays in the contract.

_migrateToDex()

Auto-trigger on realSeiReserves × seiUsdPrice ≥ graduationMcapUsd.

  1. Take 1% of accumulated jpSEI as the graduation fee, transfer to the factory address. Factory holds it as protocol revenue.
  2. Approve the Saphyre router for the remaining 99% jpSEI + all curve-side TOKEN.
  3. Call addLiquidity(TOKEN, jpSEI, ..., to: 0xdEaD). The LP receipt is minted directly to 0xdEaD. No address has a withdrawal path.

JellyPadFactory

Owner-controlled factory.

Constructor

constructor(
    address _treasury,
    address _spSeiPool,    // = JellyPadStaker
    address _spSeiToken,   // = JellyPadStaker (same)
    address _dexRouter
)

Fee routing

The factory routes incoming native-SEI fees based on operatorFeeBps:

  • Creation fee (0.01 SEI per token): 100% to staker buffer (or treasury if stakerBufferSink unset).
  • Protocol fees (1% buy / 1% sell): split. operatorFeeBps to treasury (default 5000 = 50%), the remainder to staker buffer. Bounded by MAX_OPERATOR_FEE_BPS = 7000 so the owner can never raise above 70%.
  • Graduation fee (1% of accumulated jpSEI): comes in as jpSEI, not native SEI. Stays at the factory address as protocol revenue, converted to native SEI via the 21-day slow-unstake path so the buffer is never drained for team revenue.

Key methods

MethodCallerEffect
createToken(...) payableanyone (with 0.01 SEI)Deploys new token
setTreasury(addr)ownerUpdates treasury
setSpSei(pool, token)ownerUpdates the staker addresses on the factory
setDexRouter(addr)ownerSwaps DEX router
setStakerBufferSink(addr)ownerRoutes fee inflow to staker buffer
setOperatorFeeBps(uint256)ownerBounded by MAX_OPERATOR_FEE_BPS
setTokenSeiUsdPrice(token, price)ownerUpdates oracle on a deployed token
transferOwnership(addr)ownerHands off control
withdrawERC20(token, to, amount)ownerSweep accumulated ERC-20s. Cannot touch LP shares (those go to 0xdEaD directly).
withdrawSei(to, amount)ownerSweep accidentally-trapped native SEI (fees normally pass through to treasury).

Graduation-revenue pipeline (v5)

The factory accumulates jpSEI from each graduation's 1% fee and converts it to native SEI for the team via the 21-day slow-unstake path. Buffer is never drained by team-revenue extraction — slow-unstake pulls from totalDelegated, not bufferSei.

MethodCallerEffect
receiveGraduationFee(amount)token onlyNotification hook fired by _migrateToDex after the jpSEI transfer; emits an event for indexers.
withdrawGraduationRevenue(to, amount)ownerDirect transfer of factory-held jpSEI to a recipient. Bypasses the slow path.
queueGraduationUnstake(amount)ownerBurn factory's jpSEI on the staker, queue 21-day undelegation. Reverts if a previous cycle is still pending.
claimGraduationRevenue()ownerAfter the unbond elapses, sweep the matured native SEI directly to treasury (suppresses receive()'s fee-split for the duration).
pendingGraduationUnstake()viewAmount of jpSEI currently queued.
graduationUnstakeReadyAt()viewUnix timestamp when the queue matures (0 if no cycle in flight).

Constants

  • CREATION_FEE = 0.01 ether
  • DEFAULT_GRAD_MCAP_USD = 69_000e18
  • MAX_OPERATOR_FEE_BPS = 7000 (70% cap)

JellyPadRouter

Post-graduation swap router. Wraps staking + Saphyre swap into a single user transaction. Token contract stays a clean ERC-20 with no transfer side-effects.

Key methods

MethodCallerEffect
buy(token, minTokensOut, deadline) payableanyoneTake 1% fee to factory; stake remaining SEI to jpSEI; swap jpSEI to token on Saphyre
sell(token, tokenAmount, minSeiOut, deadline)anyone (after token.approve(router, amount))Pull token; swap for jpSEI; instantUnstake to native SEI; take 1% protocol fee; transfer net to caller
quoteBuy(token, seiIn) viewanyoneToken-out estimate for a given SEI-in
quoteSell(token, tokenAmount) viewanyoneNet-SEI estimate (accounts for staker exit fee + protocol 1%)
rescueERC20(token, to, amount)ownerPull stranded ERC-20s. Router is stateless by design — this is a safety net only.
rescueNative(to, amount)ownerPull stranded native SEI. Same reasoning.
transferOwnership(addr)ownerHand off control of the rescue functions.

Constants

  • PROTOCOL_FEE = 100 bps (1%) per leg
  • All addresses (factory, SPSEI_POOL, SPSEI_TOKEN, DEX_ROUTER) are immutable. To repoint at a new factory or staker, redeploy the router.
  • owner is set to the deployer at construction; only used for the rescue paths above.

Test coverage

~109 tests across 7 suites (16 net new in v5; pre-existing v1 legacy + a flaky-seed staker test fail on main, unrelated):
  test/JellyPadStaker.t.sol               stake / unstake / claim / dynamic buffer / tiers / harvest
  test/StakerIntegration.t.sol            end-to-end with real staker
  test/JellyPadToken.t.sol              curve buy/sell, fees, graduation, LP burn
  test/GraduationRevenuePipeline.t.sol  ✓ NEW — 21d unstake pipeline + buffer-untouched invariant + rescues
  test/JellyPadRouter.t.sol          8 ✓   routed buy/sell, allowance, slippage
  test/StakingPrecompile.t.sol       2 ✓   precompile-call gating
  test/SeiLunchToken.t.sol          15 ✓ + 4 ✗   v1 legacy (deprecated; failures don't block)

Run from the contracts directory:

cd contracts && forge test

The 4 legacy v1 failures are pre-existing soft-fail bugs in deprecated v1 contracts that don't ship. The current v4 stack (Staker, Factory, Token, Router) passes 100%.