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 atMAX_OVERFLOW_CHECKPOINTS)._getRewardPrefixBeforebinary-searches both arrays. When the overflow array itself fills it becomes a ring buffer with FIFO eviction of entries older thanMAX_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 mutatingethPerVeand leaves the ETH inpendingShareholderETHuntil 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-emptyflushPendingShareholderETH) 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.timestampaftercheckpointTotalVe(). - If
globalLastTs()is still stale, flushing defers — ETH stays inpendingShareholderETHuntil 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
pendingShareholderETHuntil FIFO eviction becomes safe again. - Manual / permissionless flushes still use
MIN_VE_FLUSHfor 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 -= distributedHow 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
pendingShareholderETHwhendelta == 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_FLUSHwhen there is already a processed shareholder denominator. pendingShareholderETHis 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
- constant weight =
- Decaying lock:
- only epochs with
rewardTs < lockEndcount - accrues from historical flush timestamps, not from current
veBalanceOf(user) - prefix sums over
deltaanddelta * timestampkeep settlement bounded and gas-safe
- only epochs with
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 asclaimableEthLiveanduserEthPerVePaidfor clarity; the latter matches the storage variable name.
- Note: the Solidity return values are named
Important:
claimableEthLiveis the authoritative live preview- do not recompute
claimable + userVe * (ethPerVe - paid) / ACCoffchain - 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 isonlyClaimAllHelper).- 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,pausedtokenIddestination- 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_DURATIONfor AutoMax
minCadenceSeconds(minimum seconds between successful compounds)minEthToCompound(skip dust amounts)maxSlippageBps0= useDEFAULT_AUTOCOMPOUND_MAX_SLIPPAGE_BPS(v1.0.0: 500 = 5%)- must be
<= 2_000(20%); revertsSlippageTooHighabove 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, ...)(requiresP_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)
- auth fails closed on bundle drift and also requires Furnace + MineCore to agree on one canonical
Execution surfaces:
- compoundFor(user)
- compoundForMany(users[], maxUsers) (bounded per call)
Slippage model (important):
- Executors do not pass
minVeOut. - ShareholderRoyalties computes
minVeOutfrom a Furnace spot quote and the user’s storedmaxSlippageBps. - If
veOut > 0but floor-rounding would produceminVeOut == 0, the contract clamps it to1before calling Furnace. - If the quote fails:
compoundFor(user)reverts rather than executing without a floorcompoundForMany(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
- Canonical bundle check (
Pause reasons (analytics codes, Constants):
| Code | Reason |
|---|---|
| 1 | NOT_OWNER |
| 2 | LISTED |
| 3 | EXPIRED |
| 4 | INVALID_TOKEN_ID |
| 5 | FURNACE_REVERT |
| 6 | QUOTE_FAILED |
| 7 | CHECKPOINT_FAILED |
Integrator guidance:
compoundForManyis best-effort;compoundForis 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 toto. - Mode LOCK_FURNACE (
mode = 1):tomust equalmsg.sender(the lock is always created for the caller). Reverts withNotAuthorizedotherwise. - Emits both
ShareholderClaim(user, mode, amount)andShareholderClaimed(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
- Locks (veCLAIM) — lock lifecycle and ve decay
- Furnace — lock bonus engine and Collect & Lock path
- ClaimAllHelper — bundled collect calls
- Core Mechanics — takeover loop and Baron allocation
- Tutorial: Collect Barons ETH or Collect & Lock
- User manual: The Furnace — Royalties