Finishing up the Budget splitting work from the original PR #10844, which we broke down into multiple PRs, with this being left.
Full spec: https://hackmd.io/@jonasW3F/rkN6BXE2ex
What's left
Today, transfer_validator_incentive in pallet-staking-async does a direct Currency::transfer. We need to replace it with a vested transfer so the incentive unlocks gradually.
Recommended approach
We can't create an unbounded number of vesting schedules, so rewards within an epoch need to batch together. In the original PR, I did this by putting a (custom staking) hold on the reward for eras where era % 28 != 0, and then when era % 28 == 0 releasing the hold and creating a vesting schedule. It works but adds a lot of unneeded complexity into staking which we can offload to vesting pallet itself.
Each epoch (28 eras) gets a single vesting schedule per validator. The schedule's starting_block is the block at which that epoch began, i.e. the last era boundary where era % BondingDuration == 0. Every incentive payout within the epoch merges back into that schedule.
- Eras 28..55 → all merge into the schedule whose
starting_block is era 28's block.
- Eras 56..83 → all merge into the schedule whose
starting_block is era 56's block.
- …and so on.
Concretely:
- At each era boundary, if
era % BondingDuration == 0, snapshot the current block into a Staking::VestingEpochStart storage item.
- On every incentive payout, call pallet-vesting to create or merge to existing schedule.
Today the VestedPayout trait only has vested_transfer (always creates a new schedule). We'll need to extend it with something like add_to_vesting(source, dest, amount, duration, start_at) and implement the merge inside pallet-vesting.
Hard requirements
VestingDuration = 0 must still work (liquid payout). Config-switchable, no vesting when set to zero.
- Stay within
MaxVestingSchedules = 28. With BondingDuration = 28 eras ≈ 28 days and VestingDuration = 365 days, a new schedule is added per epoch and vests for a year. So a validator holds ~13 active incentive schedules.
Finishing up the Budget splitting work from the original PR #10844, which we broke down into multiple PRs, with this being left.
Full spec: https://hackmd.io/@jonasW3F/rkN6BXE2ex
What's left
Today,
transfer_validator_incentiveinpallet-staking-asyncdoes a directCurrency::transfer. We need to replace it with a vested transfer so the incentive unlocks gradually.Recommended approach
We can't create an unbounded number of vesting schedules, so rewards within an epoch need to batch together. In the original PR, I did this by putting a (custom staking) hold on the reward for eras where
era % 28 != 0, and then whenera % 28 == 0releasing the hold and creating a vesting schedule. It works but adds a lot of unneeded complexity into staking which we can offload to vesting pallet itself.Each epoch (28 eras) gets a single vesting schedule per validator. The schedule's
starting_blockis the block at which that epoch began, i.e. the last era boundary whereera % BondingDuration == 0. Every incentive payout within the epoch merges back into that schedule.starting_blockis era 28's block.starting_blockis era 56's block.Concretely:
era % BondingDuration == 0, snapshot the current block into aStaking::VestingEpochStartstorage item.Today the
VestedPayouttrait only hasvested_transfer(always creates a new schedule). We'll need to extend it with something likeadd_to_vesting(source, dest, amount, duration, start_at)and implement the merge inside pallet-vesting.Hard requirements
VestingDuration = 0must still work (liquid payout). Config-switchable, no vesting when set to zero.MaxVestingSchedules = 28. WithBondingDuration = 28 eras ≈ 28 daysandVestingDuration = 365 days, a new schedule is added per epoch and vests for a year. So a validator holds ~13 active incentive schedules.