NodeRegistry
Manages the active node set, staking, unbonding, and slashing/rewards plumbing for Sylan nodes. It exposes a compact view for UIs and an admin surface for governance. The contract is UUPS‑upgradeable, ownable, pausable, and uses ReentrancyGuard and SafeERC20.
Time units are seconds (Unix epoch via
block.timestamp). Token amounts use 18 decimals (SYL).
Responsibilities
- Accept SYL stakes from nodes and track lifecycle status:
None → Active ↔ Unbonding ↔ Inactive. - Enforce
minStakefor Active status; manage unbonding viaunbondingPeriod. - Maintain payout address and free‑form metadata per node.
- Provide a public, minimal node view for dashboards and eligibility checks (
isActiveNode). - Allow authorized slashers (owner/consensus/allow‑list) to slash stake and distribute to treasury / nodePool / burn by BPS.
- Track aggregate totals (
totalStaked) and optional reputation.
Key parameters & addresses (views)
sylToken() → address— SYL ERC‑20 (proxy).minStake() → uint96— minimum stake to beActive.unbondingPeriod() → uint64— seconds a node must wait after requesting to unstake.treasury() → address— project treasury.slashDestination() → address— address receiving the treasury share of slashed funds.nodePool() → address— address receiving the node‑pool share of slashed funds.treasuryBps() → uint16,nodePoolBps() → uint16,burnBps() → uint16— must sum to 10,000.totalStaked() → uint256— sum of all stakes.reputation(node) → uint256— monotonic counter; increased by Consensus for honest behavior.
Status model
None not registered
Active meets minStake and not unbonding
Unbonding pending unstake requested, waiting period running
Inactive registered but below minStake (or unbonded)Node view & eligibility
isActiveNode(node) → bool—trueifstatus == Active.nodeOf(node) → NodeInfo— compact struct for UIs:
struct NodeInfo {
uint96 stake; // current stake
uint64 unbondRequestedAt; // 0 if none
NodeStatus status; // None/Active/Unbonding/Inactive
address payout; // SYL withdrawal address
string metadata; // free‑form (e.g., endpoint, region)
}Internally, the contract also tracks
pendingUnbond(requested amount awaiting finalize). It is not part ofNodeInfoto keep reads light.
Node actions (writes)
Register & stake
registerAndStake(uint96 amount, address payout, string metadata)
Requires amount ≥ minStake. Pulls SYL from the caller, sets payout (defaults to caller if 0x0), stores metadata, sets status Active, resets any unbonding fields, and bumps totalStaked.
Emits: NodeRegistered(node, payout, initialStake, metadata), NodeActivated(node)
Stake more
stakeMore(uint96 amount)
Increases stake and totalStaked. If the node was Inactive/Unbonding but now meets minStake with no pending unbond, status flips to Active.
Emits: StakeIncreased(node, amount, newStake) (and possibly NodeActivated(node)).
Update payout / metadata
setPayout(address payout)→ updates payout (non‑zero).setMetadata(string metadata)→ updates metadata blob.
Emits: PayoutUpdated(node, payout), MetadataUpdated(node, metadata)
Unstake (two‑step with cancel)
requestUnstake(uint96 amount)
Adds topendingUnbond. IfpendingUnbond > 0, status becomes Unbonding andunbondRequestedAtis set if it was zero. Remaining effective stake isstake - pendingUnbond; if it falls belowminStake, the node is deactivated.
Emits: UnstakeRequested(node, amount, unlockAt) where unlockAt = unbondRequestedAt + unbondingPeriod.
cancelUnstake()
ClearspendingUnbondandunbondRequestedAt. If stake ≥minStake, status flips back to Active.
Emits: UnstakeCanceled(node) (and possibly NodeActivated(node)).
finalizeUnstake()
Callable afterblock.timestamp ≥ unbondRequestedAt + unbondingPeriod. Transfers the pending amount topayout(or the node if payout is zero), zeros unbond state, reducesstakeandtotalStaked, and updates status based on the remaining stake.
Emits: UnstakeFinalized(node, amount, remainingStake) (and possibly NodeActivated(node) or NodeDeactivated(node, stake)).
Slashing & reputation (protocol)
increaseReputation(address node, uint256 amount)— only Consensus may call; incrementsreputation.slash(address node, uint96 amount, bytes32 reason)— only authorized slashers may call. Slashes up to the node’s current stake. Adjusts status (InactiveorUnbondingif belowminStake).- Slashed amount is distributed:
treasuryShare = amount * treasuryBps / 10_000→ sent toslashDestination.burnShare = amount * burnBps / 10_000→ burned viaSYL.burn.nodePoolShare = amount − treasuryShare − burnShare→ sent tonodePool.
- Slashed amount is distributed:
Emits: Slashed(node, amountSlashed, reason, by)
Admin (owner) surface
- Parameters
setMinStake(uint96 newMin)→ EmitsMinStakeUpdated(old, new)setUnbondingPeriod(uint64 seconds)→ EmitsUnbondingPeriodUpdated(old, seconds)
- Wiring
setConsensus(address c)→ also (re)authorizescas slasher; EmitsConsensusUpdated(old, c)setSlasher(address slasher, bool allowed)→ allow/deny additional slasher EOA/contracts; EmitsSlasherUpdated(slasher, allowed)setTreasury(address t)→ EmitsTreasuryUpdated(old, t)setSlashDestination(address d)→ EmitsSlashDestinationUpdated(old, d)setNodePool(address p)→ EmitsNodePoolUpdated(old, p)setSlashDistribution(uint16 treasuryBps, uint16 nodePoolBps, uint16 burnBps)→ must sum to 10,000; EmitsSlashDistributionUpdated(oldTreasury, oldNodePool, oldBurn, treasuryBps, nodePoolBps, burnBps)
- Ops:
pause()/unpause()and UUPSupgradeTo/upgradeToAndCall(...)(owner‑gated)
Events (node lifecycle & ops)
NodeRegistered(address indexed node, address payout, uint96 initialStake, string metadata)NodeActivated(address indexed node)NodeDeactivated(address indexed node, uint96 stake)StakeIncreased(address indexed node, uint96 amount, uint96 newStake)PayoutUpdated(address indexed node, address payout)MetadataUpdated(address indexed node, string metadata)UnstakeRequested(address indexed node, uint96 amount, uint64 unlockAt)UnstakeCanceled(address indexed node)UnstakeFinalized(address indexed node, uint96 amount, uint96 remainingStake)Slashed(address indexed node, uint96 amount, bytes32 reason, address indexed by)
Admin events
MinStakeUpdated(uint96 oldMinStake, uint96 newMinStake)UnbondingPeriodUpdated(uint64 oldSeconds, uint64 newSeconds)ConsensusUpdated(address oldConsensus, address newConsensus)SlasherUpdated(address slasher, bool allowed)TreasuryUpdated(address oldTreasury, address newTreasury)SlashDestinationUpdated(address oldDestination, address newDestination)NodePoolUpdated(address oldNodePool, address newNodePool)SlashDistributionUpdated(uint16 oldTreasuryBps, uint16 oldNodePoolBps, uint16 oldBurnBps, uint16 newTreasuryBps, uint16 newNodePoolBps, uint16 newBurnBps)
Minimal ABI (client‑facing)
For staking dashboards and operator portals:
[
{"type":"function","stateMutability":"view","name":"isActiveNode","inputs":[{"name":"node","type":"address"}],"outputs":[{"type":"bool"}]},
{"type":"function","stateMutability":"view","name":"nodeOf","inputs":[{"name":"node","type":"address"}],"outputs":[{"type":"tuple","components":[
{"name":"stake","type":"uint96"},
{"name":"unbondRequestedAt","type":"uint64"},
{"name":"status","type":"uint8"},
{"name":"payout","type":"address"},
{"name":"metadata","type":"string"}
]}]},
{"type":"function","stateMutability":"nonpayable","name":"registerAndStake","inputs":[{"name":"amount","type":"uint96"},{"name":"payout","type":"address"},{"name":"metadata","type":"string"}],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"stakeMore","inputs":[{"name":"amount","type":"uint96"}],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"setPayout","inputs":[{"name":"payout","type":"address"}],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"setMetadata","inputs":[{"name":"metadata","type":"string"}],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"requestUnstake","inputs":[{"name":"amount","type":"uint96"}],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"cancelUnstake","inputs":[],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"finalizeUnstake","inputs":[],"outputs":[]}
]Integration tips
- Approvals: Nodes must
approve(registry, amount)on SYL beforeregisterAndStakeorstakeMore. - Countdowns: show
unlockAtfromUnstakeRequestedto drive a UX timer (unbondingPeriodmay change between requests; honor event data). - Status logic: treat
Activeas eligible; displayUnbondingdistinctly and disable new votes if your operator policy requires it. - Payout safety: encourage setting a dedicated payout address (can be a multisig) via
setPayout.
Security & invariants
- BPS sum = 10,000 across
{treasuryBps, nodePoolBps, burnBps}. - Authorized slashing only: owner, consensus, and allow‑listed slashers can call
slash. - Pause blocks writes (
register/stake/unstake/slash), but reads continue. - Reentrancy protected around transfers and finalization.
Out of scope for this page
- Consensus rules & rewards distribution logic → APIConsensus / APIEscrow
- Token details → Sylan Token
- Addresses & ABIs → Architecture → Addresses & ABIs