Skip to content

Commit 0c757fa

Browse files
committed
docs(drip): document rate-preservation logic in _addFees
1 parent 10e9b30 commit 0c757fa

File tree

2 files changed

+15
-6
lines changed

2 files changed

+15
-6
lines changed

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,13 @@ This prevents two attack vectors:
131131

132132
#### How it works
133133

134-
1. **Fee enters pipeline** -- when a user unstakes, the full `rnbwValue` is removed from `totalPooledRnbw`. The exit fee is passed to `_addFees()`, which adds it to `undistributedFees` and calculates `rewardRate = undistributedFees / dripDuration`.
135-
2. **Linear drip** -- `_syncPool()` is called before every state-changing operation. It calculates `earned = elapsed * rewardRate`, moves that amount from `undistributedFees` into `totalPooledRnbw`, and emits `ExchangeRateUpdated`.
134+
1. **Fee enters pipeline** -- when a user unstakes, the full `rnbwValue` is removed from `totalPooledRnbw`. The exit fee is passed to `_addFees()`, which adds it to `undistributedFees` and sets `rewardRate` and `dripEndTime`.
135+
2. **Linear drip** -- `_syncPool()` is called before every state-changing operation. It calculates `earned = elapsed * rewardRate`, moves that amount from `undistributedFees` into `totalPooledRnbw`, and emits `ExchangeRateUpdated`. When the full drip window has elapsed, all remaining `undistributedFees` are flushed at once (avoiding dust from integer division), and `rewardRate`/`dripEndTime` are zeroed.
136136
3. **View functions** -- `getExchangeRate()`, `getRnbwForShares()`, `previewUnstake()`, etc. use `_effectivePooledRnbw()` which simulates the pending drip without mutating state, so the frontend always shows the accurate current value.
137-
4. **Overlapping drips** -- if a second unstake happens mid-drip, `_syncPool()` settles what's owed so far, then `_addFees()` combines the remaining undistributed fees with the new exit fee and restarts the drip window: `rewardRate = (remaining + newFee) / dripDuration`.
137+
4. **Overlapping drips (rate preservation)** -- if a second unstake happens mid-drip, `_syncPool()` settles what's owed so far, then `_addFees()` applies rate-preservation logic:
138+
- **No active drip** (`block.timestamp >= dripEndTime`): start a fresh cycle — `rewardRate = undistributedFees / dripDuration`, new 7-day window.
139+
- **Rate goes up** (`proposedRate >= rewardRate`): the new fee is large enough to increase the drip speed — reset the full drip window with the higher rate.
140+
- **Rate stays flat** (`proposedRate < rewardRate`): the new fee is small (e.g., dust unstake) — keep the current `rewardRate` and extend `dripEndTime` just enough to distribute the remaining fees at the current speed. This prevents an attacker from repeatedly dust-unstaking to reset the 7-day window and delay fee distribution.
138141

139142
```
140143
Day 0: Bob unstakes, 5,000 RNBW exit fee → undistributedFees = 5,000
@@ -144,11 +147,15 @@ Day 3: Next operation triggers _syncPool()
144147
earned = 3 * 714.3 ≈ 2,143 RNBW moved to pool
145148
undistributedFees ≈ 2,857
146149
147-
Day 3: Charlie unstakes, 2,000 RNBW exit fee
150+
Day 3: Charlie unstakes, 2,000 RNBW exit fee (large → rate goes up)
148151
undistributedFees = 2,857 + 2,000 = 4,857
149-
rewardRate = 4,857 / 7 days ≈ 694 RNBW/day (fresh 7-day window)
152+
proposedRate = 4,857 / 7 days ≈ 694 ≥ 714? No → keep rate at 714
153+
dripEndTime extended to Day 3 + (4,857 / 714) ≈ Day 9.8
150154
151-
Day 10: Drip complete, undistributedFees ≈ 0
155+
Day 3: (alternatively, if Charlie's fee were larger, e.g. 10,000 RNBW)
156+
undistributedFees = 2,857 + 10,000 = 12,857
157+
proposedRate = 12,857 / 7 days ≈ 1,837 ≥ 714? Yes → reset window
158+
rewardRate = 1,837 RNBW/day, fresh 7-day window from Day 3
152159
```
153160

154161
Key invariant: `totalPooledRnbw + undistributedFees` always equals the total RNBW that belongs to stakers.

src/RNBWStaking.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,8 @@ contract RNBWStaking is IRNBWStaking, ReentrancyGuard, Pausable, EIP712 {
532532
} else {
533533
// Small fee added — keep current rate, extend window by just enough
534534
// time to distribute the remaining fees at the current speed.
535+
// Note: integer division may slightly undershoot; _syncPool flushes
536+
// all remaining undistributedFees once block.timestamp >= dripEndTime.
535537
dripEndTime = block.timestamp + (undistributedFees / rewardRate);
536538
}
537539
}

0 commit comments

Comments
 (0)