You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+13-6Lines changed: 13 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -131,10 +131,13 @@ This prevents two attack vectors:
131
131
132
132
#### How it works
133
133
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.
136
136
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.
138
141
139
142
```
140
143
Day 0: Bob unstakes, 5,000 RNBW exit fee → undistributedFees = 5,000
@@ -144,11 +147,15 @@ Day 3: Next operation triggers _syncPool()
144
147
earned = 3 * 714.3 ≈ 2,143 RNBW moved to pool
145
148
undistributedFees ≈ 2,857
146
149
147
-
Day 3: Charlie unstakes, 2,000 RNBW exit fee
150
+
Day 3: Charlie unstakes, 2,000 RNBW exit fee (large → rate goes up)
0 commit comments