Skip to content

Commit 4c1ed3a

Browse files
committed
fix(N-04): cap emergencyWithdraw at available excess instead of reverting
1 parent 27ad2f5 commit 4c1ed3a

File tree

3 files changed

+19
-3
lines changed

3 files changed

+19
-3
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ Where:
416416
- Dead shares: 1000 shares minted to `0xdead` on first deposit (prevents share inflation / first depositor attack)
417417
- Cashback reserve: tracked separately from staking pool, protected from `emergencyWithdraw`
418418
- Staking reserve: tracked separately, protected from `emergencyWithdraw`, reclaimable via `defundStakingReserve()`
419-
- Emergency withdraw: for RNBW, only excess above `totalPooledRnbw + cashbackReserve + stakingReserve + undistributedFees` can be withdrawn -- the pool, both reserves, and pending drip fees are untouchable. Non-RNBW tokens have no restriction (rescue for accidental sends).
419+
- Emergency withdraw: for RNBW, the transfer is capped at excess above `totalPooledRnbw + cashbackReserve + stakingReserve + undistributedFees` -- if the requested amount exceeds excess, it is silently capped (check `EmergencyWithdrawn` event for actual amount). The pool, both reserves, and pending drip fees are untouchable. Non-RNBW tokens pass through uncapped (rescue for accidental sends).
420420
- Min stake floor: `minStakeAmount` cannot be set below 1 RNBW
421421
- Inflation guard: `ZeroSharesMinted` revert protects depositors from rounding attacks
422422
- Residual sweep: when only dead shares remain, `totalPooledRnbw` and `undistributedFees` are swept to safe and pool is reset
@@ -542,7 +542,7 @@ Backend responsibility: filter amounts that would mint 0 shares at current rate,
542542

543543
### No admin access to pool or reserve
544544

545-
There is no function that lets the admin withdraw from `totalPooledRnbw`. Pool RNBW is only withdrawable by stakers burning shares. Cashback reserve is consumable via signed allocations or reclaimable via `defundCashbackReserve()`. Staking reserve is consumable via `stakeForWithSignature` or reclaimable via `defundStakingReserve()`. `emergencyWithdraw` is restricted to excess RNBW above all four tracked pools (`totalPooledRnbw + cashbackReserve + stakingReserve + undistributedFees`). To wind down the protocol: stop new stakes (disable frontend / revoke signers), let existing users unstake normally, residual sweeps to safe when pool empties. Note: `pause()` is an emergency brake that freezes everything including unstaking -- it is not a wind-down mechanism.
545+
There is no function that lets the admin withdraw from `totalPooledRnbw`. Pool RNBW is only withdrawable by stakers burning shares. Cashback reserve is consumable via signed allocations or reclaimable via `defundCashbackReserve()`. Staking reserve is consumable via `stakeForWithSignature` or reclaimable via `defundStakingReserve()`. `emergencyWithdraw` caps RNBW transfers at excess above all four tracked pools (`totalPooledRnbw + cashbackReserve + stakingReserve + undistributedFees`) -- requested amounts above excess are silently capped. To wind down the protocol: stop new stakes (disable frontend / revoke signers), let existing users unstake normally, residual sweeps to safe when pool empties. Note: `pause()` is an emergency brake that freezes everything including unstaking -- it is not a wind-down mechanism.
546546

547547
## Future: Liquid Staking Token (xRNBW)
548548

src/RNBWStaking.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,8 @@ contract RNBWStaking is IRNBWStaking, ReentrancyGuard, Pausable, EIP712 {
349349
uint256 balance = RNBW_TOKEN.balanceOf(address(this));
350350
uint256 reserved = totalPooledRnbw + cashbackReserve + stakingReserve + undistributedFees;
351351
uint256 excess = balance > reserved ? balance - reserved : 0;
352-
if (amount > excess) revert InsufficientExcess();
352+
if (excess == 0) revert InsufficientExcess();
353+
amount = amount > excess ? excess : amount;
353354
}
354355
IERC20(token).safeTransfer(safe, amount);
355356
emit EmergencyWithdrawn(token, amount);

test/RNBWStaking.t.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,21 @@ contract RNBWStakingTest is Test {
546546
assertEq(rnbwToken.balanceOf(admin), 50 ether);
547547
}
548548

549+
function test_EmergencyWithdrawCapsAtExcess() public {
550+
vm.startPrank(alice);
551+
rnbwToken.approve(address(staking), 100 ether);
552+
staking.stake(100 ether);
553+
vm.stopPrank();
554+
555+
rnbwToken.mint(address(staking), 30 ether);
556+
557+
uint256 adminBefore = rnbwToken.balanceOf(admin);
558+
vm.prank(admin);
559+
staking.emergencyWithdraw(address(rnbwToken), 100 ether);
560+
561+
assertEq(rnbwToken.balanceOf(admin) - adminBefore, 30 ether);
562+
}
563+
549564
function test_EmergencyWithdrawRevertInsufficientExcess() public {
550565
vm.startPrank(alice);
551566
rnbwToken.approve(address(staking), 100 ether);

0 commit comments

Comments
 (0)