RateLimited.sol#L118 RateLimitedIsm.sol#L13 RateLimitedHook.sol#L32
A vulnerability exists in the contract that inherit the RateLimited contract, allowing an attacker to bypass intended rate-limiting access-control mechanisms.
Because the validateAndConsumeFilledLevel() function is publicly callable, anyone can exhaust the rate limit, causing a denial of service for legitimate transactions. This affects both the RateLimitedHook and RateLimitedIsm contracts that inherit from RateLimited.
The RateLimited contract implements a token bucket algorithm for rate limiting. However, it exposes the core rate-limiting function validateAndConsumeFilledLevel() as a public function without access control:
function validateAndConsumeFilledLevel(
uint256 _consumedAmount
) public returns (uint256) {
uint256 adjustedFilledLevel = calculateCurrentLevel();
require(_consumedAmount <= adjustedFilledLevel, "RateLimitExceeded");
// Reduce the filledLevel and update lastUpdated
uint256 _filledLevel = adjustedFilledLevel - _consumedAmount;
filledLevel = _filledLevel;
lastUpdated = block.timestamp;
emit ConsumedFilledLevel(filledLevel, lastUpdated);
return _filledLevel;
}This allows any external actor to call this function directly and consume the entire available capacity (i.e., filledLevel), effectively performing a denial-of-service attack on both:
- RateLimitedHook - preventing legitimate token transfers from being dispatched
- RateLimitedIsm - preventing legitimate message verification
The vulnerability is demonstrated in the provided test cases:
function testRateLimitedHook_allowsTransfer_ifUnderLimit(
uint128 _amount,
uint128 _time
) external {
// Warp to a random time, get it's filled level, and try to transfer less than the target max
vm.warp(_time);
uint256 filledLevelBefore = rateLimitedHook.calculateCurrentLevel();
rateLimitedHook.validateAndConsumeFilledLevel(filledLevelBefore);
uint256 limitAfter = rateLimitedHook.calculateCurrentLevel();
assertEq(limitAfter, 0);
}
function testRateLimitedIsm_verify(uint128 _amount) external {
vm.assume(_amount <= rateLimitedIsm.calculateCurrentLevel());
rateLimitedIsm.validateAndConsumeFilledLevel(rateLimitedIsm.calculateCurrentLevel());
vm.prank(address(localMailbox));
localMailbox.process(bytes(""), _encodeTestMessage(_amount));
}In both cases, an attacker can call validateAndConsumeFilledLevel() with the current available capacity (obtained via calculateCurrentLevel()), exhausting the rate limit completely. After this attack, legitimate transfers will be blocked until the rate limit refills according to the defined refillRate.
Severity: High
Likelihood: High
Impact: High
This vulnerability enables an attacker to:
- Block all cross-chain token transfers for some time (up to DURATION, which is 1 day)
- Prevent message verification in the ISM contract
- Force users to wait for rate limit refill or pay higher gas prices to compete with the attacker
- Disrupt the regular operation of the protocol with minimal cost
- The attack can be performed repeatedly, creating a persistent denial of service condition for the affected contracts.
Implement proper access control on the validateAndConsumeFilledLevel() function to restrict its usage to authorized callers only.