Bot Sessions (DelegationHub)
DelegationHub is the onchain session registry for opt-in bot delegation. It lets users grant time-limited, permission-scoped sessions to bot addresses, which protocol contracts verify on every delegated call. In the CLAIM stream, this is how bots automate takeovers, harvesting, Furnace entries, and lock maintenance on behalf of users.
TL;DR: Users call
setSession(delegate, perms, expiry)or sign gasless via EIP-712. Bots are checked withisAuthorized(user, delegate, requiredPerms). 18 permission bits covering Crown, Harvest, Furnace, VeLock, and Config actions. Sessions should be short-lived with minimal perms.
A session is:
user(the identity being represented)delegate(the bot / executor)perms(permission bitmask)expiry(unix seconds)
Sessions are verified by protocol contracts onchain via isAuthorized(...).
Contract + wiring
Source:
src/DelegationHub.solsrc/interfaces/IDelegationHub.solsrc/lib/DelegationPermissions.sol(canonical bit positions)
Wiring (v1.0.0+):
Every delegated path fails closed on bundle drift (see Wiring safety model). No contract trusts a raw delegationHub() read in isolation — each resolves the hub through cross-checks against the live bundle:
- MineCore / Furnace: each stores
delegationHub; authorization requires the other contract to agree on the same hub and the same CLAIM / ve roots. - ClaimAllHelper: resolves the hub from MineCore / ShareholderRoyalties / Furnace; rejects split-brain
MineCore.furnace()vsShareholderRoyalties.furnace()wiring. - VeClaimNFT: safe-maintenance wrappers resolve the hub through Furnace + MarketRouter + MineCore.
- ShareholderRoyalties / LpStakingVault7D: resolve through Furnace + MineCore cross-checks.
- MarketRouter: does not use DelegationHub in v1.0.0 (market actions use standard approvals, not delegation).
Wiring rule: set the hub address before going live. The setter is onlyOwner, governed through the live timelock owner path.
Read surface
| Method | Notes |
|---|---|
getSession(user, delegate) -> (perms, expiry) | Raw stored session data |
isAuthorized(user, delegate, requiredPerms) -> bool | Checks expiry and bitmask |
nonces(user) -> uint256 | EIP-712 nonce for gasless set-by-sig. Also increments on direct setSession(...) / revokeSession(...) to invalidate previously prepared signatures. |
Write surface
| Method | Caller | Notes |
|---|---|---|
setSession(delegate, perms, expiry) | user | Stores session; overwrites prior |
revokeSession(delegate) | user | Clears perms + expiry |
setSessionBySig(user, delegate, perms, expiry, nonce, deadline, sig) | anyone | Gasless; validates signature + nonce + deadline |
Signature model:
- EIP-712 domain:
- name:
ClaimRush DelegationHub - version:
1
- name:
- Uses OpenZeppelin
SignatureChecker(supports EOAs + EIP-1271 smart wallets). - Direct onchain
setSession(...)andrevokeSession(...)also incrementnonces(user).- Integrators MUST re-read the current nonce after any direct session change.
- Any previously prepared
SetSessionsignature becomes invalid once a direct update/revoke lands onchain.
Consumers in protocol contracts
Important:
- protocol consumers do not all trust a raw stored
delegationHubpointer in isolation - MineCore resolves the canonical hub through the live Furnace + MineCore bundle before checking session bits
- ClaimAllHelper, VeClaimNFT, ShareholderRoyalties, and LpStakingVault7D apply similar consumer-specific live wiring checks
MineCore: delegated takeover
MineCore.takeoverFor(newKing, maxPrice):
msg.senderpays ETH and becomes the executornewKingbecomes the King identity- requires
DelegationHub.isAuthorized(newKing, msg.sender, P_TAKEOVER_FOR)against the canonically resolved hub
Default routing for the new reign:
ethRecipient = msg.sender(bot)claimRecipient = newKing(user)
Optional:
- if the session also grants
P_ROUTE_REIGN_CLAIM_TO_CALLER, MineCore sets:claimRecipient = msg.sender
Mid-reign routing update:
MineCore.setCurrentReignRecipients(ethRecipient, claimRecipient)- MineCore again resolves the canonical hub through the live Furnace + MineCore bundle before checking any session bits
- callable by the active King identity
- or an authorized delegate with:
- for
ethRecipient:P_SET_REIGN_ETH_RECIPIENTorP_SET_REIGN_ETH_RECIPIENT_TO_CALLER_ONLY - for
claimRecipient:P_SET_REIGN_CLAIM_RECIPIENTorP_SET_REIGN_CLAIM_RECIPIENT_TO_USER_ONLY
- for
ClaimAllHelper: delegated harvest + withdraw
ClaimAllHelper provides delegation-gated wrappers:
claimShareholderForUser(user, ...)requiresP_CLAIM_SHAREHOLDER_FORwithdrawKingBalanceForUser(user)requiresP_WITHDRAW_KING_BUCKET_FORclaimAllFor(user, ...)requiresP_CLAIM_ALL_FOR
Furnace: delegated entry (bot pays, user receives lock)
Delegated entry points:
enterWithEthFor(user, ...)requiresP_FURNACE_ENTER_ETH_FORenterWithClaimFromCallerFor(user, ...)requiresP_FURNACE_ENTER_CLAIM_FORenterWithTokenFromCallerFor(user, ...)requiresP_FURNACE_ENTER_TOKEN_FOR
VeClaimNFT: delegated lock maintenance (safe, non-custodial)
Delegated lock maintenance entry points:
Furnace.extendWithBonusFor(user, tokenId, durationSeconds, minBonusOut)requiresP_VE_EXTEND_LOCK_FORmergeLocksForUser(user, fromTokenId, intoTokenId)requiresP_VE_MERGE_LOCKS_FORunlockExpiredForUser(user, tokenId)requiresP_VE_UNLOCK_EXPIRED_FOR
Non-custodial rules enforced:
- no ERC20 spend from the user
- unlock returns CLAIM to
user(never to the delegate) - merge/extend require the lock(s) are owned by
user
Settings/config: delegated config setters (safe, non-custodial)
Delegated config entry points:
MineCore.setKingAutoLockConfigForUser(user, ...)requiresP_SET_KING_AUTO_LOCK_CONFIG_FORShareholderRoyalties.setAutoCompoundConfigForUser(user, ...)requiresP_SET_SHAREHOLDER_AUTOCOMPOUND_CONFIG_FOR- The callee resolves the canonical hub from the live Furnace / MineCore pair and also rejects split-brain Baron bundles where MarketRouter / MineCore / Claim roots drift away from the active ShareholderRoyalties surface.
LpStakingVault7D.setAutoCompoundConfigForUser(user, ...)requiresP_SET_LP_AUTOCOMPOUND_CONFIG_FOR
Non-custodial rules enforced:
- config-only (no value transfer)
- tokenId parameters must be veNFTs owned by
user
Permissions map
Canonical bits are defined in src/lib/DelegationPermissions.sol.
| Group | Bit | Permission | Enables |
|---|---|---|---|
| Crown | 0 | P_TAKEOVER_FOR | MineCore.takeoverFor(newKing, maxPrice) |
| Crown | 1 | P_ROUTE_REIGN_CLAIM_TO_CALLER | Route King-stream mined CLAIM to bot during delegated takeover |
| Crown | 2 | P_SET_REIGN_ETH_RECIPIENT | Allow delegate to set ethRecipient mid-reign to any address |
| Crown | 3 | P_SET_REIGN_ETH_RECIPIENT_TO_CALLER_ONLY | Allow delegate to set ethRecipient = msg.sender mid-reign |
| Crown | 4 | P_SET_REIGN_CLAIM_RECIPIENT | Allow delegate to set claimRecipient mid-reign to any address |
| Crown | 5 | P_SET_REIGN_CLAIM_RECIPIENT_TO_USER_ONLY | Allow delegate to set claimRecipient = king mid-reign |
| Harvest | 6 | P_WITHDRAW_KING_BUCKET_FOR | ClaimAllHelper.withdrawKingBalanceForUser(user) |
| Harvest | 7 | P_CLAIM_SHAREHOLDER_FOR | ClaimAllHelper.claimShareholderForUser(user, ...) |
| Harvest | 8 | P_CLAIM_ALL_FOR | ClaimAllHelper.claimAllFor(user, ...) |
| Furnace | 9 | P_FURNACE_ENTER_ETH_FOR | Furnace.enterWithEthFor(user, ...) |
| Furnace | 10 | P_FURNACE_ENTER_CLAIM_FOR | Furnace.enterWithClaimFromCallerFor(user, ...) |
| Furnace | 11 | P_FURNACE_ENTER_TOKEN_FOR | Furnace.enterWithTokenFromCallerFor(user, ...) |
| VeLock | 12 | P_VE_EXTEND_LOCK_FOR | Furnace.extendWithBonusFor(user, tokenId, durationSeconds, minBonusOut) |
| VeLock | 13 | P_VE_MERGE_LOCKS_FOR | VeClaimNFT.mergeLocksForUser(user, fromTokenId, intoTokenId) |
| VeLock | 14 | P_VE_UNLOCK_EXPIRED_FOR | VeClaimNFT.unlockExpiredForUser(user, tokenId) |
| Config | 15 | P_SET_KING_AUTO_LOCK_CONFIG_FOR | MineCore.setKingAutoLockConfigForUser(user, ...) |
| Config | 16 | P_SET_SHAREHOLDER_AUTOCOMPOUND_CONFIG_FOR | ShareholderRoyalties.setAutoCompoundConfigForUser(user, ...) |
| Config | 17 | P_SET_LP_AUTOCOMPOUND_CONFIG_FOR | LpStakingVault7D.setAutoCompoundConfigForUser(user, ...) |
Integration guidance
- Keep sessions short-lived (hours/days), refresh as needed.
- Offchain tooling should not treat a raw stored
delegationHubpointer as sufficient; protocol consumers may reject a session if live wiring drift breaks their canonical hub resolution. - Use minimal perms for the bot’s job.
- Treat recipient routing perms as high risk:
P_SET_REIGN_ETH_RECIPIENTcan redirect the dethroned-King ETH payout for the active reign.P_SET_REIGN_CLAIM_RECIPIENTcan redirect mined CLAIM mid-reign.- Prefer constrained variants (
...TO_CALLER_ONLY,...TO_USER_ONLY) where possible.
- Treat delegation as security-sensitive UX (like approvals):
- show the delegate address
- show expiry
- show selected permissions
- provide a one-click revoke
Agent SDK
This repo’s TypeScript SDK (agents/sdk/) includes helpers for DelegationHub:
- canonical permission bits (
agents/sdk/src/delegation/permissions.ts) - EIP-712 typed data builder (
buildSetSessionTypedData) - sign + submit
setSessionBySig(signSetSession,submitSetSessionBySig)
Examples:
# Local demo (user signs with derived actor0; delegate submits with derived actor1)
RPC_URL=http://127.0.0.1:8545 npm -C agents/sdk run example:delegation
# Production-style: user wallet signs typed data; delegate submits signature
RPC_URL=http://127.0.0.1:8545 \
npm -C agents/sdk run example:session -- --cmd build \
--user 0xUserAddress \
--delegate 0xDelegateAddress \
--perms TAKEOVER_FOR,CLAIM_ALL_FOR \
--out /tmp/session.json \
--pretty
# user signs /tmp/session.json via eth_signTypedData_v4
RPC_URL=http://127.0.0.1:8545 PRIVATE_KEYS=0x<delegatePrivateKey> \
npm -C agents/sdk run example:session -- --cmd submit --typed-data /tmp/session.json --sig 0x...
# Revoke (gasless)
RPC_URL=http://127.0.0.1:8545 \
npm -C agents/sdk run example:session -- --cmd build --revoke --user 0xUserAddress --delegate 0xDelegateAddress --out /tmp/revoke.json --pretty
RPC_URL=http://127.0.0.1:8545 PRIVATE_KEYS=0x<delegatePrivateKey> \
npm -C agents/sdk run example:session -- --cmd submit --typed-data /tmp/revoke.json --sig 0x...
# Run a delegated agent loop
RPC_URL=http://127.0.0.1:8545 npm -C agents/sdk run example:agent -- --actor-index 1 --acting-for 0xUserAddress --once
# Delegated safe maintenance (ve upkeep + optional config sync)
RPC_URL=http://127.0.0.1:8545 \
ENABLE_SAFE_MAINTENANCE=1 \
VE_EXTEND_IF_REMAINING_DAYS=7 \
VE_EXTEND_BY_DAYS=30 \
npm -C agents/sdk run example:agent -- --actor-index 1 --acting-for 0xUserAddress --once
Optional config sync env vars:
- `KING_AUTO_LOCK_*` (MineCore King auto-lock config)
- `ROYALTIES_AUTOCOMPOUND_*` (ShareholderRoyalties auto-compound config)
- `LP_AUTOCOMPOUND_*` (LP vault auto-compound config)Observability and indexing
DelegationHub emits SessionSet(user, delegate, perms, expiry) on:
setSession(...)revokeSession(delegate)(stored asperms=0,expiry=0)setSessionBySig(...)
Expiry notes (important for integrators):
isAuthorized(...)returnsfalseifexpiry == 0orexpiry <= block.timestamp(expired sessions are invalid at the exact second of expiry, not one second later).- No “never-expires” session value exists in v1.0.0.
perms != 0withexpiry == 0orexpiry <= block.timestampreverts (Errors.DeadlineExpired()). Enforced in_setSession(...)andsetSessionBySig(...)to prevent unreachable sessions from polluting storage.
In addition, protocol contracts emit Events.DelegationSessionUsed(user, delegate, actionTypeId, permsUsed, refId, timestamp)
when a delegated entrypoint succeeds.
actionTypeId is a numeric uint8 defined in src/lib/DelegationActionTypes.sol:
| actionTypeId | Constant | Subgraph category | refId meaning |
|---|---|---|---|
| 1 | TAKEOVER_FOR | TAKEOVER | newReignId |
| 2 | MINECORE_SET_REIGN_RECIPIENTS | REIGN_RECIPIENTS | reignId |
| 10 | CLAIM_SHAREHOLDER_FOR | CLAIM | 0 |
| 11 | WITHDRAW_KING_BUCKET_FOR | CLAIM | 0 |
| 12 | CLAIM_ALL_FOR | CLAIM | 0 |
| 20 | FURNACE_ENTER_WITH_ETH_FOR | FURNACE_ENTER | tokenIdUsed |
| 21 | FURNACE_ENTER_WITH_CLAIM_FOR | FURNACE_ENTER | tokenIdUsed |
| 22 | FURNACE_ENTER_WITH_TOKEN_FOR | FURNACE_ENTER | tokenIdUsed |
| 30 | VE_EXTEND_LOCK_FOR | VE_LOCK | tokenId |
| 31 | VE_MERGE_LOCKS_FOR | VE_LOCK | intoTokenId (destination) |
| 32 | VE_UNLOCK_EXPIRED_FOR | VE_LOCK | tokenId |
| 40 | MINECORE_SET_KING_AUTO_LOCK_CONFIG_FOR | CONFIG | destination tokenId (0 when disabling / not yet pinned) |
| 41 | SHAREHOLDER_SET_AUTOCOMPOUND_CONFIG_FOR | CONFIG | tokenId (0 when disabling) |
| 42 | LP_STAKING_SET_AUTOCOMPOUND_CONFIG_FOR | CONFIG | tokenId (0 when disabling) |
Subgraph behavior (this repo):
- The subgraph stores both:
actionTypeId(exact numeric id; canonical for audits/analytics)actionType(coarse enum for UI), derived by id ranges (seesubgraph/src/utils/delegation.ts)
- Raw
actionTypeIdis canonical. The shipped coarse mapper insubgraph/src/utils/delegation.tsmapsactionTypeId = 2(MINECORE_SET_REIGN_RECIPIENTS) toactionType = REIGN_RECIPIENTS. - Rolling session state is kept in
DelegationSession(includinglastUsedAt,lastActionType,lastTxHash). - Full history is recorded in
DelegationSessionUse(immutable, tx hashes included). - Session change events are recorded in
DelegationSessionSetEvent(immutable, tx hashes included).
These entities power:
- Security → Bot access (sessions list + activity feed + one-click revoke)
- Radar Inbox alerts (bot session used, session granted/updated/revoked, recipients changed mid-reign)
See also
- Agents and Automation — SDK, CRAL pack, and agent architecture
- Maintenance and Bots — keeper loops that consume sessions
- Security, Guardian, Pausing — guardian controls and pause states
- Tutorial: Run a Crown bot
- User manual: Bots & Automation