Skip to Content
Royalties (Barons)

ShareholderRoyalties (Barons)

ShareholderRoyalties distributes ETH from takeovers to veCLAIM holders (Barons). It maintains a per-ve ETH index and supports two claim modes: Collect ETH (liquid) and Collect & Lock (compound through the Furnace). In the CLAIM stream, this is where the 25% takeover ETH flows to locked CLAIM holders and optionally compounds back into veCLAIM.

ETH allocation

On every takeover, MineCore allocates ETH to ShareholderRoyalties:

  • genesis takeover (prevKing == 0x0): 100% of pricePaid
  • otherwise: 25% of pricePaid

The contract tracks:

  • pendingShareholderETH: ETH waiting to be indexed (typically zero-holder carry, rounding dust, temporary carry while the ve checkpoint is still catching up to the current block timestamp, or deferred carry while checkpoint storage cannot safely represent a new reward timestamp)
  • ethPerVe: global index, scaled by ACC = 1e18
  • reward checkpoints keyed by flush timestamp so delayed claims for decaying locks stay correct. Once the main array reaches MAX_REWARD_CHECKPOINTS (50,000) it is frozen and subsequent flushes append to a bounded overflow array (_overflowCheckpoints, capped at MAX_OVERFLOW_CHECKPOINTS). _getRewardPrefixBefore binary-searches both arrays. When the overflow array itself fills it becomes a ring buffer with FIFO eviction of entries older than MAX_LOCK_DURATION. If every overflow entry is still inside the active lock horizon and the new flush has a distinct timestamp, ShareholderRoyalties defers the flush before mutating ethPerVe and leaves the ETH in pendingShareholderETH until the oldest overflow entry ages out. Same-block writes still coalesce in place when the timestamp matches exactly.

Runtime hardening:

  • All state-changing royalties paths (onTakeover, addPendingShareholderETH, non-empty flushPendingShareholderETH) fail closed on bundle drift (see Wiring safety model).
  • This prevents MineCore from silently indexing takeover ETH into a stale royalties surface.

Index formula (ethPerVe)

Constants:

  • ACC = 1e18
  • SHAREHOLDER_WEIGHT_SCALE = 1e18
  • SHAREHOLDER_ACC = 1e36
  • MIN_VE_FLUSH = 100e18 (minimum total ve for manual / residual flushes)

Takeover allocations are auto-attempted immediately whenever a processed denominator exists, even below MIN_VE_FLUSH:

  • They index in the takeover tx only when ve.globalLastTs() == block.timestamp after checkpointTotalVe().
  • If globalLastTs() is still stale, flushing defers — ETH stays in pendingShareholderETH until the checkpoint reaches the current block.
  • If checkpoint storage is saturated and cannot safely represent a new distinct reward timestamp, flushing also defers — ETH stays in pendingShareholderETH until FIFO eviction becomes safe again.
  • Manual / permissionless flushes still use MIN_VE_FLUSH for leftover pending ETH.
  • Non-empty flushes fail closed on bundle drift (Errors.WiringMismatch()). See Wiring safety model.

Flushing moves ETH from pendingShareholderETH into ethPerVe:

ve.checkpointTotalVe() # REQUIRED before reading processed ve-bias state rewardTs = ve.globalLastTs() if rewardTs != now: do nothing # defer until the ve checkpoint fully catches up # totalWeight is the processed total ve-bias used internally by VeClaimNFT. totalWeight = ve.totalVeBiasScaled() # units: ve * 1e18 if totalWeight == 0: do nothing if ceil(totalWeight / 1e18) < MIN_VE_FLUSH: do nothing if reward checkpoint storage cannot safely represent rewardTs: do nothing # delta = floor(pending * SHAREHOLDER_ACC / totalWeight) delta = floor(pendingShareholderETH * 1e36 / totalWeight) if delta == 0: do nothing ethPerVe += delta ethPerVeTimeWeighted += delta * rewardTs store_reward_checkpoint(rewardTs, ethPerVe, ethPerVeTimeWeighted) # if main array is at MAX_REWARD_CHECKPOINTS, routes to _overflowCheckpoints distributed = floor(delta * totalWeight / 1e36) pendingShareholderETH -= distributed

How reward settlement works:

  • Decaying ve locks cannot be settled safely from claim-time veBalanceOf(user) alone.
  • ShareholderRoyalties records historical flush timestamps and reconstructs each user’s delayed rewards from those timestamps.
  • This prevents under-accrual and stranded ETH for decaying, non-AutoMax locks.

Why MIN_VE_FLUSH exists:

  • It gates manual/residual flushes when rewards could not be indexed immediately.
  • It keeps rounding dust in pendingShareholderETH when delta == 0.
  • It matters after bounded ve checkpointing: if globalLastTs() is stale, flushing defers and the ETH remains pending.
  • Checkpoint-history retention also matters: if the overflow ring is saturated inside the active lock horizon, flushing defers instead of writing an unsafe checkpoint approximation.

Practical implication for UIs:

  • Takeover ETH does not wait for MIN_VE_FLUSH when there is already a processed shareholder denominator.
  • pendingShareholderETH is mostly a residual bucket (zero-shareholder carry, rounding dust, brief carry while the ve checkpoint catches up, or temporary carry while checkpoint storage waits for a safe eviction window).

User accounting

Per user:

  • userEthPerVePaid[user]
  • claimableEth[user] (stored ETH already crystallized onchain)
  • userTimeWeightedEthPerVePaid[user]
  • userLastRewardTs[user]
  • userRewardRemainder[user] (sub-wei carry bucket)

checkpointUser(user):

  • reconstructs unprocessed reward epochs from VeClaimNFT.getShareholderLockParams(user)
  • uses the current lock set plus historical reward checkpoints
  • is safe because VeClaimNFT checkpoints ShareholderRoyalties before every ve mutation (create, add, extend, merge, toggle AutoMax, unlock, transfer for settlement)
  • fails closed on bundle drift (see Wiring safety model) — this ensures MarketRouter settlement checkpoints the correct royalties contract before ownership moves

Lock treatment:

  • AutoMax lock:
    • constant weight = amount
    • accrues across all unprocessed epochs
  • Decaying lock:
    • only epochs with rewardTs < lockEnd count
    • accrues from historical flush timestamps, not from current veBalanceOf(user)
    • prefix sums over delta and delta * timestamp keep settlement bounded and gas-safe

Rounding:

  • ETH distribution remains floor-rounded
  • per-user sub-wei fractions are carried in userRewardRemainder[user] instead of being silently lost

MarketRouter integration:

  • MarketRouter calls checkpointTransfer(from, to) before moving a veNFT into Furnace custody (Sell now / listing settlement) so royalties accounting stays correct.

UI display (read-only) without checkpointUser

For dashboards and UIs, avoid calling checkpointUser(user) just to render a number (it is state-changing).

Read:

  • getShareholderState(user) -> (claimableEthLive, userVe, userEthPerVePaid)
    • Note: the Solidity return values are named (claimable, userVe, paid). Documentation aliases them as claimableEthLive and userEthPerVePaid for clarity; the latter matches the storage variable name.

Important:

  • claimableEthLive is the authoritative live preview
  • do not recompute claimable + userVe * (ethPerVe - paid) / ACC offchain
  • that shortcut is wrong for decaying locks because it ignores historical flush timestamps

Notes:

  • The live preview still excludes ETH sitting in pendingShareholderETH (not yet flushed into the index). In normal gameplay this bucket should mostly be zero-holder carry, tiny dust, or brief carry while the ve checkpoint catches up.
  • A claim transaction checkpoints internally first, so you do not need to checkpoint separately.
  • Reference implementation: frontend/src/lib/shareholderPendingEth.ts (if present in the frontend tree).

Collecting (claimShareholder)

ShareholderRoyalties exposes a single entry for users:

  • claimShareholder(mode, targetTokenId, durationSeconds, createAutoMax, minVeOut)

Modes:

  • mode 0: send ETH to user (UI verb: Collect)
  • mode 1: route ETH through Furnace.lockEthReward (ETH → CLAIM + Furnace bonus, locked to veCLAIM)

UI recommendation (matches frontend):

  • Primary CTA: mode 0 (Collect ETH). Secondary: mode 1 (Collect & Lock) when Furnace.lockingPaused == false.
  • When Furnace.lockingPaused == true, mode 0 (Collect ETH) is the only enabled path.

Notes:

  • The contract name and method names use “claim”. UI copy MUST use Collect for ETH.
  • All state-changing claim paths fail closed on bundle drift (see Wiring safety model). If wiring is wrong, the call reverts and claimable ETH is preserved.

Helper-only entrypoint:

  • claimShareholderFor(user, ...) exists for ClaimAllHelper bundling (and is onlyClaimAllHelper).
  • If you want a one-tx “Collect + withdraw King bucket” flow or delegation-gated Collect wrappers, use:

Auto-compound (owner-or-keeper-allowlisted)

This is an opt-in config. Compounding for an opted-in user is owner-or-keeper-allowlisted: the normal executor is an allowlisted Baron compound keeper, and owner break-glass remains available for recovery.

Config fields (stored per user):

  • enabled, paused
  • tokenId destination
    • MUST be an existing eligible user-owned lock at config time
    • v1 auto-compound never creates a new lock as a fallback; invalid destinations pause instead
  • durationSeconds (stored; v1.0.0 execution note)
    • validated when saving config (MIN..MAX; AutoMax requires MAX)
    • adding capital does not change the lock’s duration
    • effective duration at execution: current remaining for normal locks, or MAX_LOCK_DURATION for AutoMax
  • minCadenceSeconds (minimum seconds between successful compounds)
  • minEthToCompound (skip dust amounts)
  • maxSlippageBps
    • 0 = use DEFAULT_AUTOCOMPOUND_MAX_SLIPPAGE_BPS (v1.0.0: 500 = 5%)
    • must be <= 2_000 (20%); reverts SlippageTooHigh above this cap
  • lastCompoundTs (read-only; updated on success)

_validateAutoCompoundDestination returns (false, EXPIRED) when a non-AutoMax destination lock has less than MIN_LOCK_DURATION (7 days) remaining.

Config surfaces:

  • setAutoCompoundConfig(...)
  • Delegation-gated setter: setAutoCompoundConfigForUser(user, ...) (requires P_SET_SHAREHOLDER_AUTOCOMPOUND_CONFIG_FOR)
    • auth fails closed on bundle drift and also requires Furnace + MineCore to agree on one canonical DelegationHub (see Wiring safety model)

Execution surfaces:

  • compoundFor(user)
  • compoundForMany(users[], maxUsers) (bounded per call)

Slippage model (important):

  • Executors do not pass minVeOut.
  • ShareholderRoyalties computes minVeOut from a Furnace spot quote and the user’s stored maxSlippageBps.
  • If veOut > 0 but floor-rounding would produce minVeOut == 0, the contract clamps it to 1 before calling Furnace.
  • If the quote fails:
    • compoundFor(user) reverts rather than executing without a floor
    • compoundForMany(users[], maxUsers) skips that user and continues
  • Offchain maintainer calls SHOULD submit auto-compound txs via a private relay for MEV protection; owner break-glass should do the same when practical.

Wiring safety:

  • compoundFor(user): atomic — reverts on quote failure, bundle drift, or Furnace call failure. Claimable ETH and cadence state stay untouched on revert.
  • compoundForMany(users[], maxUsers): best-effort per user:
    • Canonical bundle check (_requireCanonicalBaronRuntimeBundle) runs once at the top — reverts the whole call if wiring is broken
    • Checkpoint failure → pauses that user (reason CHECKPOINT_FAILED = 7), continues batch
    • Quote failure → skips that user (emits ShareholderAutoCompoundFailed), does NOT pause, continues batch
    • Furnace call failure → pauses that user (reason FURNACE_REVERT = 5), restores user accounting, continues batch

Pause reasons (analytics codes, Constants):

CodeReason
1NOT_OWNER
2LISTED
3EXPIRED
4INVALID_TOKEN_ID
5FURNACE_REVERT
6QUOTE_FAILED
7CHECKPOINT_FAILED

Integrator guidance:

  • compoundForMany is best-effort; compoundFor is atomic and may revert.
  • UIs should surface pause reasons and provide a one-click fix (delist, extend, update tokenId).

Batch checkpoint

checkpointUserBatch(address[] calldata users, uint256 maxUsers) crystallises accrued ETH rewards for multiple users in a single transaction, capped by maxUsers. Each iteration calls this.checkpointUser(user) via try/catch — any individual revert is silently skipped and the batch continues processing remaining users (best-effort semantics).

Claim to alternate receiver

claimShareholderTo(address payable to, uint8 mode, uint256 targetTokenId, uint256 durationSeconds, bool createAutoMax, uint256 minVeOut) claims baron ETH rewards to a specified receiver address. This allows smart-contract wallets whose primary address cannot receive ETH to redirect claims to an EOA or compatible receiver.

  • Mode ETH (mode = 0): sends claimed ETH to to.
  • Mode LOCK_FURNACE (mode = 1): to must equal msg.sender (the lock is always created for the caller). Reverts with NotAuthorized otherwise.
  • Emits both ShareholderClaim(user, mode, amount) and ShareholderClaimed(user, to, amount, mode) (includes destination).

The ShareholderClaimed event is the canonical receipt for indexers tracking claim destinations. It is only emitted by claimShareholderTo, not by the original claimShareholder / claimShareholderFor paths.

See also