-
Notifications
You must be signed in to change notification settings - Fork 109
Description
Bug Description
Found an issue in the rewards module where it forces at least 1 coin to be released per block, even when the math says it should be less. This breaks long-term reward schedules.
Look at x/rewards/types/reward.go line 42:
// Make sure at least one coin will be distributed if there is still a distribution to happen
amountToRelease = math.MaxInt(amountToRelease, math.NewInt(1))The problem: when you have a long vesting schedule (like 1 year), each block should only release a tiny fraction of a coin. But this line forces it to always release at least 1 coin, so the pool drains way faster than it should.
How to reproduce
I tested with a simple schedule:
- Total: 1000 tokens
- Duration: 1,000,000 seconds (about 11.5 days)
- Block time: 6 seconds
Math says each block should release: (6/1,000,000) * 1000 = 0.006 tokens
But the code forces minimum 1 token per block.
So after 100 blocks (600 seconds):
- Expected: 0.6 tokens released
- Actual: 100 tokens released
That's 166x faster than it should be!
What should happen vs what actually happens
The calculation works like this:
timeElapsed = 6 seconds
totalDuration = 1,000,000 seconds
proportion = 6/1,000,000 = 0.000006
amountToRelease = 1000 * 0.000006 = 0.006
This truncates to 0 (because coins are integers), then the MaxInt line forces it to 1.
Result: pool drains in 1000 blocks instead of 166,666 blocks.
Environment
Tested on Ubuntu 22.04 with Go 1.23.5
Impact
This breaks any long-term reward program. Example with 1 year vesting:
- 1M tokens over 365 days
- Block time 6 seconds
- Should release ~0.19 tokens per block
- Actually releases 1 token per block
- Finishes in 70 days instead of 365 days
The pool drains 5x faster than planned, completely breaking the tokenomics.
Test Case
func TestRewardOverDistribution(t *testing.T) {
totalAmount := sdk.NewCoin("akii", math.NewInt(1000))
startTime := time.Now()
endTime := startTime.Add(1000000 * time.Second)
schedule := types.ReleaseSchedule{
TotalAmount: totalAmount,
ReleasedAmount: sdk.NewCoin("akii", math.ZeroInt()),
LastReleaseTime: startTime,
EndTime: endTime,
Active: true,
}
totalReleased := math.ZeroInt()
currentTime := startTime
for i := 0; i < 100; i++ {
currentTime = currentTime.Add(6 * time.Second)
reward, err := types.CalculateReward(currentTime, schedule)
require.NoError(t, err)
totalReleased = totalReleased.Add(reward.Amount)
schedule.ReleasedAmount = sdk.NewCoin("akii", totalReleased)
schedule.LastReleaseTime = currentTime
}
// After 100 blocks (600 seconds), should release ~0.6 tokens
// But actually releases 100 tokens
require.Equal(t, math.NewInt(100), totalReleased)
}Test Results
$ cd /root/kiichain && /root/kiichain/go/bin/go test -v ./x/rewards/types -run TestRewardOverDistribution
=== RUN TestRewardOverDistribution
reward_over_distribution_test.go:51: Total released after 100 blocks: 100
reward_over_distribution_test.go:52: Expected release: ~0.6 tokens
reward_over_distribution_test.go:53: Actual release: 100 tokens
--- PASS: TestRewardOverDistribution (0.00s)
PASS
Declaration
- Not a duplicate
- Tested and confirmed
- Understand false reports are rejected