Withdrawal Rate Limiter

How the protocol caps fund outflows to survive an exploit — burst window, linear refill, bypass addresses, and owner force reset.

Smart contract exploits rarely drain a vault instantaneously. Most attacks are iterative: the attacker finds a bug, calls the vulnerable function repeatedly, and accumulates the stolen funds over several transactions. The window between the first suspicious withdrawal and a full drain is often minutes or hours — long enough to act, if the protocol can create that window.

The withdrawal rate limiter is Perpl's circuit breaker. It places a hard cap on how quickly funds can leave the protocol, expressed as a percentage of total value locked per hour. In a hack, the attacker is limited to draining at most that percentage per cycle, regardless of how many times they call the withdraw function. Operators have at least one full cycle — typically 60 minutes — to detect the exploit, freeze the attacker, and begin recovery before the vault is meaningfully depleted.


The Hourly Limit

Every withdrawal attempt is checked against a live allowance. That allowance is constrained by an hourly limit, computed from two protocol parameters:

hourlyLimit = max( wrlsThousandthsTvl × TVL / 1000, minWithdrawLimitCNS ) Default: wrlsThousandthsTvl = 100 (10% of TVL), minWithdrawLimitCNS = $1,000,000

The first term scales the limit with the vault: a larger protocol allows more absolute throughput. The floor ensures the limit never drops so low that legitimate users are routinely blocked — even at modest TVL, at least $1M per hour can flow out. Both parameters are owner-configurable and can be adjusted as the protocol scales.

Critically, the hourly limit is recomputed at the start of each new cycle from current TVL. If an attacker drains the vault across multiple cycles, each subsequent cycle starts with a lower TVL, reducing the absolute limit. At $10M TVL the limit is $1M/hr. After draining $4M over four cycles, TVL is $6M and the computed rate is $600k/hr — but the floor of $1M still applies. The floor prevents the "spiral" where a small account becomes effectively unwithdrawable.


Burst Window and Linear Refill

The hourly limit is not made immediately available in full. Instead, the protocol splits each cycle into two phases. This prevents an attacker from draining the entire hour's allocation in a single transaction at the start of the cycle.

At the start of a new cycle, the contract makes approximately 25% of the hourly limit available upfront as the burst amount. This accommodates normal user behavior — most withdrawals are routine and well within a reasonable burst window. After that burst is consumed or the first 15 minutes elapse, the remaining 75% unlocks gradually, block by block, at a rate of hourlyLimit / 8,571 tokens per block (8,571 blocks = one hour at Monad's 0.42 s block time).

perBlock = hourlyLimit / 8,571 ≈ $117/block at $1M hourly limit
burst ≈ hourlyLimit × (2,143 / 8,571) ≈ 25% of hourlyLimit 2,143 blocks = 15 minutes. Burst covers the first 15-minute window upfront.
Figure 1: Theoretical Withdrawal Allowance Over One Cycle
Theoretical withdrawal allowance over one cycle: an upfront burst followed by linear refill until the cycle resets. ▲ BURST ZONE hourly limit ~25% burst consumed $1.00M $750k $500k $250k $0 0 min 15 min 30 min 45 min 60 min burst window ends linear refill begins Theoretical (no withdrawals) Actual (after withdrawals)
At $1M/hr, the burst provides $250k upfront. The remaining $750k unlocks at ~$117/block over the next 45 minutes. An attacker who drains the burst immediately must then wait for block-by-block refill before withdrawing more.

The contract tracks when the current burst window ends via _wrlsLastBlock. If a withdrawal arrives before that block, no additional refill is added — the allowance is exactly what was set at cycle start. Once past _wrlsLastBlock, the contract accrues the elapsed blocks' worth of refill before checking the request.


How Cycles Work

The rate limiter is stateful. A "cycle" is a 60-minute period initialized when the first withdrawal arrives after the previous cycle expires. The contract does not proactively start or end cycles — it is entirely reactive, updating state only when a withdrawal is attempted.

On each withdrawal call, the contract first determines which phase it is in:

No active cycle (block > _wrlsExpiryBlock): A new cycle starts. The hourly limit is computed from current TVL, the per-block rate is set, and the burst allowance is loaded. The expiry block is set 8,571 blocks (one hour) in the future.

Within burst window (block ≤ _wrlsLastBlock): No refill is accrued. The allowance is exactly what was set at cycle start, minus any withdrawals already made.

Linear refill phase (_wrlsLastBlock < block ≤ _wrlsExpiryBlock): The contract adds (block − _wrlsLastBlock) × perBlock to the allowance and advances _wrlsLastBlock. The withdrawal is then checked against this updated allowance.

Figure 2: Cycle State Transitions
IDLE
No active cycle
expiryBlock = 0
First withdrawal
triggers new cycle
ACTIVE
allowance = burst
lastBlock = now + BP15
expiry = now + BPH
owner forceReset()
zeros all state
RESET
allowance = 0
perBlock = 0
expiry = 0
lastBlock = 0
block advances
past lastBlock
REFILLING
allowance += elapsed
         × perBlock
lastBlock = now
block > expiryBlock
on next withdrawal
CYCLE EXPIRES
New cycle starts
limit recomputed
from current TVL
The reset path (owner forceResetWithdrawRateLimit()) zeroes all four state variables. The next incoming withdrawal will treat the contract as if no cycle has ever run, starting a completely fresh cycle from the current TVL snapshot.
Lazy evaluation: the rate limiter never runs on a schedule. No keeper, no cron job, no gas spent proactively. State updates only when a withdrawal is attempted. A vault with no withdrawals for two hours does not automatically carry over any "saved" allowance — the next withdrawal starts a fresh cycle regardless.
▶ Explore the interactive simulation

Surviving an Exploit

The rate limiter's primary purpose is to bound the damage from a contract exploit. Without it, an attacker who discovers a reentrancy bug or unauthorized withdrawal vector can drain an entire vault in a single block. With it, the maximum damage in any 60-minute window is capped at the hourly limit — typically 10% of TVL.

The burst window creates an additional chokepoint. Even at cycle start, when the allowance is highest, an attacker can only extract the burst amount (~25% of the hourly limit) before the rate limiter forces them to wait for the linear drip. Their maximum theoretical throughput from a cold start is:

max_instant = burst ≈ 0.25 × hourlyLimit An attacker who hits the contract at cycle start gets at most 25% of the hourly limit before being blocked.

The remaining 75% of any given cycle's capacity is locked behind the block-by-block refill. An attacker willing to wait can eventually claim it — but waiting is dangerous. Each passing block is another block during which monitoring systems can detect anomalous outflow, on-chain alerts can fire, and operators can invoke the bypass lockdown or force reset.

Figure 3: Maximum Attacker Throughput — Ten Cycles
Cycle TVL at start Hourly limit Burst (instant) Max drain Response window
1$10.00M$1.00M$250k$1.00M60 min
2$9.00M$1.00M$250k$1.00M60 min
3$8.00M$1.00M$250k$1.00M60 min
4$7.00M$1.00M$250k$1.00M60 min
5$6.00M$1.00M$250k$1.00M60 min
6$5.00M$1.00M$250k$1.00M60 min
7$4.00M$1.00M$250k$1.00M60 min
8$3.00M$1.00M$250k$1.00M60 min
9$2.00M$1.00M$250k$1.00M60 min
10$1.00M$1.00M ⌊floor⌋$250k$1.00M60 min
Worst case: a fully patient attacker who waits out each complete cycle can drain at most $1M per hour. At $10M TVL the 10% rate and the $1M floor coincide. As TVL falls below $10M, the floor takes over and the absolute limit stays fixed at $1M — preventing the spiral where a depleted vault becomes trivially drainable. Each row represents a minimum 60-minute response window.

In practice, an attacker cannot drain the maximum each cycle. Partial refill, retry failures, and gas costs all reduce real throughput. The interactive simulation shows a realistic attack pattern: the burst is taken immediately, subsequent attempts are blocked, and the attacker can only extract smaller amounts as each cycle's linear refill trickles in.

Key property: The rate limiter cannot prevent a loss — it can only slow one down. Its value is the time window it creates. A protocol that responds within 60 minutes of a first anomalous withdrawal — by freezing the exploit vector, invoking the force reset, or raising bypass barriers — can cap total damage at a single cycle's allowance.

Bypass Addresses

Some protocol actors need to move funds without touching the rate limiter. The operator of a market-making vault needs to rebalance. An automated liquidation keeper needs to sweep collateral immediately. A protocol treasury contract may manage funds on a schedule that doesn't align with 60-minute cycles.

For these actors, the owner can designate bypass addresses. When _wrlsBypassAddrs[msg.sender] is true, the rate limiter check is skipped entirely — the withdrawal proceeds directly without consulting or modifying any of the four state variables. The bypass is per-address and owner-controlled.

if (_wrlsBypassAddrs[msg.sender]) return; // skip rate limiter Early return before any allowance check. Allowance, perBlock, expiryBlock, lastBlock are untouched.

Because bypass transactions do not touch the allowance, they also do not affect the refill schedule. A large bypass withdrawal by a market maker does not consume cycle capacity — subsequent non-bypass users still have the full allowance available.

Bypass addresses in an attack: An attacker who gains control of a bypass address has effectively bypassed the rate limiter entirely. Bypass designations should be treated with the same access-control rigor as owner-level permissions. In the interactive simulation, toggling bypass ON shows this behavior: the Protocol Vault and Market Maker bypass addresses move funds at full speed regardless of cycle state.

Owner Force Reset

When operators detect an exploit in progress and have frozen the attacker's account-level access, the rate limiter may still be in mid-cycle state with depleted allowance. Legitimate users — who had nothing to do with the attack — may find themselves unable to withdraw because the attacker consumed the cycle's capacity. The force reset addresses this.

Calling forceResetWithdrawRateLimit() zeroes all four state variables:

_wrlsAllowance = 0
_wrlsCollateralPerBlock = 0
_wrlsExpiryBlock = 0
_wrlsLastBlock = 0 All four fields cleared atomically. The contract behaves as if no cycle has ever run.

The next withdrawal after a force reset will trigger a completely fresh cycle, computed from current TVL at that moment. This means legitimate users immediately regain access to a full burst window. The reset is instantaneous and requires no waiting for the current cycle to expire.

In a typical response sequence: (1) exploit detected, (2) attacker's accounts frozen at the position/account level, (3) force reset called to restore withdrawal capacity for legitimate users, (4) investigation begins. The rate limiter served its purpose — it bought the time needed to execute steps 1–3.


TVL Scaling and Deposits

Because the hourly limit is a function of current TVL, the limit grows as the protocol grows. A vault with $50M TVL at 10% can sustain $5M/hr in legitimate withdrawals — appropriate for a large protocol with significant trading volume. A smaller vault at $2M is capped at the $1M floor rather than $200k/hr, keeping the protocol usable at all sizes.

Deposits made mid-cycle do not affect the current cycle's parameters. The per-block rate and expiry block are fixed at cycle start and unchanged by incoming deposits. However, when the current cycle expires and the next withdrawal triggers a new cycle, the hourly limit is recomputed from the new (higher) TVL. A $3M deposit during a $10M-TVL cycle will raise the next cycle's limit from $1M to $1.3M/hr.

Counterintuitive edge case: Deposits made during an active attack increase the attacker's capacity in subsequent cycles. If TVL rises from $9M to $12M due to a large deposit between cycle 1 and cycle 2, the attacker's next-cycle limit increases from $1M (floor) to $1.2M. The rate limiter is TVL-aware — it cannot distinguish between legitimate and adversarial contexts when computing limits. During an incident new deposits are paused so drain rate is not inadvertently boosted.
▶ Explore the interactive simulation