Hand voting to a professional delegator, or keep your position in custody and vote for yourself. Positions are held by DelegatorCustody — they leave your wallet.
{!chain.connected && Connect a wallet to see your positions.}
{error && {error}}
{/* subtabs */}
{TABS.map(([k,l]) => (
))}
{/* ── POSITIONS ── */}
{tab === 'positions' && (
STEP 1 · DEPOSIT INTO CUSTODY
Approval is granted per veGauge — one per gauge, then every position from it deposits in a click.
{loading && !data && LOADING…}
{data && wallet.length === 0 && {chain.connected ? 'NO POSITIONS LEFT IN WALLET' : 'CONNECT WALLET'}}
{wallet.map(n => {
const ok = data.approved[n.gauge.toLowerCase()];
return (
A custodied position can vote for itself, with no delegator. This is the resting state after a custody deposit and the way to reach the fee-tier program without giving up your vote.
SELF-VOTE
Allocate this position's weight across gauges yourself, once per epoch. Custodied positions appear in the Vote screen's NFT picker.
{!d.active
? No longer accepting delegations.
: votedThisEpoch
? Already voted this epoch — join in {joinCopy}.
: g && !g.joinOpen
? Group closed to new members right now.
: Open — {g ? "hasn't voted yet" : 'no group for this gauge yet, one opens on delegation'}}
Thresholds are read on-chain from FeeDiscountV4. {data && chain.connected ? <>Your registered VP: {dfmt.units(data.effectiveBalance, 18, 0)}.> : null} Your tier moves only when the manager calls registerDelegatedVP — not with your live gauge balance.
{!pickedDelegator && Pick an open delegator to continue. Rows that already voted this epoch will revert on-chain.}
{selPos?.voteActive && !selPos.delegated && This position voted this epoch. Reset its votes on the Exit tab before delegating.}
{pickedDelegator ? `DELEGATE TO ${pickedDelegator.name}` : 'SELECT A DELEGATOR'}
{dev && DelegatorVotingManager.delegateNFTs(underlying, [nftKeys], delegator, registerToFeeDiscount={String(regFee)})}
Every position exits on its own terms. What it costs you depends on where it sits right now — delegated or self-voting, registered for the fee discount or not.
{custody.map(p => {
const on = selPos?.key === p.key;
const tier = p.registered ? tierFor(tiers, p.benefitValue) : null;
const st = stateOf(p);
const route = p.registered
? { txt:'PATH B or C · registered for the fee discount', color:'#ffd47c' }
: { txt:'PATH A · immediate, no penalty', color:'#4ade80' };
return (
);
})}
{data && custody.length === 0 &&
NOTHING IN CUSTODY TO EXIT
}
{selPos && (
STEP 2 · EXIT NFT #{selPos.tokenId.toString()} · {selPos.label}
{selPos.registered ? 'REGISTERED FOR THE FEE DISCOUNT — THE LOCKUP APPLIES' : 'NOT REGISTERED — NO LOCKUP'}
)}
{selPos?.voteActive && !selPos.delegated && (
THIS POSITION HAS AN ACTIVE VOTE
Reset votes to withdraw. Withdrawal is blocked until the reset confirms.
selfReset(selPos)}>RESET VOTES
{dev && revert PositionVoteActive(nftKey, tokenId) — decoded by the tx modal · selfResetVotes(nftKey)}
)}
{selPos && !selPos.registered && (
PATH A · STRAIGHT OUT
IMMEDIATE
{selPos.delegated ? '2 transactions, no wait, no penalty. Pull out of the group, then pull out of custody.' : '1 transaction. The position goes straight back to your wallet.'}
{selPos.delegated && selPos.exit.groupVotedThisEpoch && <> The group already voted this epoch — leaving now is blocked until the epoch rolls. Use the escape hatch instead.>}
{selPos.delegated
? leaveGroup(selPos)}>1 · LEAVE GROUP
: withdrawFromCustody(selPos)}>WITHDRAW NOW}
{selPos.delegated && 2 · WITHDRAW FROM CUSTODY}
)}
{selPos?.delegated && (
PATH A′ · ESCAPE HATCH
~1 EPOCH
Your delegator went quiet? Schedule the withdrawal and execute it after one epoch. The second call is permissionless — nobody can hold your position hostage.
PATH C · OUT NOW
{selPos.exit.instantExitFeeBps ? (selPos.exit.instantExitFeeBps / 100) + '%' : '5%'} PENALTY
Force the unlock immediately. Costs {selPos.exit.instantExitFeeBps ? (selPos.exit.instantExitFeeBps / 100) + '%' : '5%'} of your tier VP and blocks re-registering any position for 2 weeks.
{!selPos.exit.canInstantExitNow && selPos.exit.blocker && <> {selPos.exit.blocker}>}
{[
['VP PENALTY', 'Taken against your tier voting power, not your tokens. Your fee tier may drop.'],
['2-WEEK COOLDOWN', "You won't be able to register ANY position for the fee discount for two weeks. This is the part people don't expect."],
].map(([t,b]) => (
{t}
{b}
))}
setForceDialog(false)}>CANCEL { setForceDialog(false); forceUnlock(selPos); }}>I ACCEPT BOTH — FORCE UNLOCK
Not settled means nobody has pulled the group's reward for that epoch yet. Settlement is permissionless once the epoch closes — anyone, including you, can trigger it.
{dev && claimGroupRewards(groupKey, internal, pool, token, epoch) · claimRewardAsMember(groupKey, bribe, pool, token, epoch, self) · claimGroupLPEmissions(groupKey, gauge, 0, 20)}
{data ? `${data.count} voted epoch${data.count === 1 ? '' : 's'} to collect.` : 'Counting voted epochs…'} Claiming walks the epochs in batches of 20 and is idempotent per epoch — safe to re-run.