Warp Route rate limiter hook & ISM (#3527)
### Description Adds rate limiting hook and ism to limit warp route transfers ### Drive-by changes <!-- Are there any minor or drive-by changes also included? --> ### Related issues - Fixes #2697 ### Backward compatibility Yes ### Testing Unit Tests --------- Signed-off-by: Paul Balaji <paul@hyperlane.xyz> Co-authored-by: Noah Bayindirli 🥂 <noah@primeprotocol.xyz> Co-authored-by: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Co-authored-by: Paul Balaji <paul@hyperlane.xyz> Co-authored-by: J M Rossy <jm.rossy@gmail.com> Co-authored-by: Trevor Porter <trkporter@ucdavis.edu> Co-authored-by: Daniel Savu <23065004+daniel-savu@users.noreply.github.com>pull/3561/head
parent
37d49ec581
commit
24e1f86d25
@ -0,0 +1,58 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
pragma solidity >=0.8.0; |
||||
import {MailboxClient} from "contracts/client/MailboxClient.sol"; |
||||
import {IPostDispatchHook} from "contracts/interfaces/hooks/IPostDispatchHook.sol"; |
||||
import {Message} from "contracts/libs/Message.sol"; |
||||
import {TokenMessage} from "contracts/token/libs/TokenMessage.sol"; |
||||
import {RateLimited} from "contracts/libs/RateLimited.sol"; |
||||
|
||||
contract RateLimitedHook is IPostDispatchHook, MailboxClient, RateLimited { |
||||
using Message for bytes; |
||||
using TokenMessage for bytes; |
||||
|
||||
mapping(bytes32 messageId => bool validated) public messageValidated; |
||||
|
||||
modifier validateMessageOnce(bytes calldata _message) { |
||||
bytes32 messageId = _message.id(); |
||||
require(!messageValidated[messageId], "MessageAlreadyValidated"); |
||||
_; |
||||
messageValidated[messageId] = true; |
||||
} |
||||
|
||||
constructor( |
||||
address _mailbox, |
||||
uint256 _maxCapacity |
||||
) MailboxClient(_mailbox) RateLimited(_maxCapacity) {} |
||||
|
||||
/// @inheritdoc IPostDispatchHook |
||||
function hookType() external pure returns (uint8) { |
||||
return uint8(IPostDispatchHook.Types.Rate_Limited_Hook); |
||||
} |
||||
|
||||
/// @inheritdoc IPostDispatchHook |
||||
function supportsMetadata(bytes calldata) external pure returns (bool) { |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Verify a message, rate limit, and increment the sender's limit. |
||||
* @dev ensures that this gets called by the Mailbox |
||||
*/ |
||||
function postDispatch( |
||||
bytes calldata, |
||||
bytes calldata _message |
||||
) external payable validateMessageOnce(_message) { |
||||
require(_isLatestDispatched(_message.id()), "InvalidDispatchedMessage"); |
||||
|
||||
uint256 newAmount = _message.body().amount(); |
||||
validateAndConsumeFilledLevel(newAmount); |
||||
} |
||||
|
||||
/// @inheritdoc IPostDispatchHook |
||||
function quoteDispatch( |
||||
bytes calldata, |
||||
bytes calldata |
||||
) external pure returns (uint256) { |
||||
return 0; |
||||
} |
||||
} |
@ -0,0 +1,53 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
import {IMailbox} from "contracts/interfaces/IMailbox.sol"; |
||||
import {MailboxClient} from "contracts/client/MailboxClient.sol"; |
||||
import {RateLimited} from "contracts/libs/RateLimited.sol"; |
||||
import {IInterchainSecurityModule} from "contracts/interfaces/IInterchainSecurityModule.sol"; |
||||
import {Message} from "contracts/libs/Message.sol"; |
||||
import {TokenMessage} from "contracts/token/libs/TokenMessage.sol"; |
||||
|
||||
contract RateLimitedIsm is |
||||
MailboxClient, |
||||
RateLimited, |
||||
IInterchainSecurityModule |
||||
{ |
||||
using Message for bytes; |
||||
using TokenMessage for bytes; |
||||
|
||||
mapping(bytes32 messageId => bool validated) public messageValidated; |
||||
|
||||
modifier validateMessageOnce(bytes calldata _message) { |
||||
bytes32 messageId = _message.id(); |
||||
require(!messageValidated[messageId], "MessageAlreadyValidated"); |
||||
messageValidated[messageId] = true; |
||||
_; |
||||
} |
||||
|
||||
constructor( |
||||
address _mailbox, |
||||
uint256 _maxCapacity |
||||
) MailboxClient(_mailbox) RateLimited(_maxCapacity) {} |
||||
|
||||
/// @inheritdoc IInterchainSecurityModule |
||||
function moduleType() external pure returns (uint8) { |
||||
return uint8(IInterchainSecurityModule.Types.UNUSED); |
||||
} |
||||
|
||||
/** |
||||
* Verify a message, rate limit, and increment the sender's limit. |
||||
* @dev ensures that this gets called by the Mailbox |
||||
*/ |
||||
function verify( |
||||
bytes calldata, |
||||
bytes calldata _message |
||||
) external validateMessageOnce(_message) returns (bool) { |
||||
require(_isDelivered(_message.id()), "InvalidDeliveredMessage"); |
||||
|
||||
uint256 newAmount = _message.body().amount(); |
||||
validateAndConsumeFilledLevel(newAmount); |
||||
|
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
||||
|
||||
/** |
||||
* @title RateLimited |
||||
* @notice A contract used to keep track of an address sender's token amount limits. |
||||
* @dev Implements a modified token bucket algorithm where the bucket is full in the beginning and gradually refills |
||||
* See: https://dev.to/satrobit/rate-limiting-using-the-token-bucket-algorithm-3cjh |
||||
**/ |
||||
contract RateLimited is OwnableUpgradeable { |
||||
uint256 public constant DURATION = 1 days; // 86400 |
||||
uint256 public filledLevel; /// @notice Current filled level |
||||
uint256 public refillRate; /// @notice Tokens per second refill rate |
||||
uint256 public lastUpdated; /// @notice Timestamp of the last time an action has been taken TODO prob can be uint40 |
||||
|
||||
event RateLimitSet(uint256 _oldCapacity, uint256 _newCapacity); |
||||
|
||||
constructor(uint256 _capacity) { |
||||
_transferOwnership(msg.sender); |
||||
setRefillRate(_capacity); |
||||
filledLevel = _capacity; |
||||
} |
||||
|
||||
error RateLimitExceeded(uint256 newLimit, uint256 targetLimit); |
||||
|
||||
/** |
||||
* @return The max capacity where the bucket will no longer refill |
||||
*/ |
||||
function maxCapacity() public view returns (uint256) { |
||||
return refillRate * DURATION; |
||||
} |
||||
|
||||
/** |
||||
* Calculates the refilled amount based on time and refill rate |
||||
* |
||||
* Consider an example where there is a 1e18 max token limit per day (86400s) |
||||
* If half of the tokens has been used, and half a day (43200s) has passed, |
||||
* then there should be a refill of 0.5e18 |
||||
* |
||||
* To calculate: |
||||
* Refilled = Elapsed * RefilledRate |
||||
* Elapsed = timestamp - Limit.lastUpdated |
||||
* RefilledRate = Capacity / DURATION |
||||
* |
||||
* If half of the day (43200) has passed, then |
||||
* (86400 - 43200) * (1e18 / 86400) = 0.5e18 |
||||
*/ |
||||
function calculateRefilledAmount() internal view returns (uint256) { |
||||
uint256 elapsed = block.timestamp - lastUpdated; |
||||
return elapsed * refillRate; |
||||
} |
||||
|
||||
/** |
||||
* Calculates the adjusted fill level based on time |
||||
*/ |
||||
function calculateCurrentLevel() public view returns (uint256) { |
||||
uint256 _capacity = maxCapacity(); |
||||
require(_capacity > 0, "RateLimitNotSet"); |
||||
|
||||
if (block.timestamp > lastUpdated + DURATION) { |
||||
// If last update is in the previous window, return the max capacity |
||||
return _capacity; |
||||
} else { |
||||
// If within the window, refill the capacity |
||||
uint256 replenishedLevel = filledLevel + calculateRefilledAmount(); |
||||
|
||||
// Only return _capacity, in the case where newCurrentCapcacity overflows |
||||
return replenishedLevel > _capacity ? _capacity : replenishedLevel; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets the refill rate by giving a capacity |
||||
* @param _capacity new maxiumum capacity to set |
||||
*/ |
||||
function setRefillRate( |
||||
uint256 _capacity |
||||
) public onlyOwner returns (uint256) { |
||||
uint256 _oldRefillRate = refillRate; |
||||
uint256 _newRefillRate = _capacity / DURATION; |
||||
refillRate = _newRefillRate; |
||||
|
||||
emit RateLimitSet(_oldRefillRate, _newRefillRate); |
||||
|
||||
return _newRefillRate; |
||||
} |
||||
|
||||
/** |
||||
* Validate an amount and decreases the currentCapacity |
||||
* @param _newAmount The amount to consume the fill level |
||||
* @return The new filled level |
||||
*/ |
||||
function validateAndConsumeFilledLevel( |
||||
uint256 _newAmount |
||||
) public returns (uint256) { |
||||
uint256 adjustedFilledLevel = calculateCurrentLevel(); |
||||
require(_newAmount <= adjustedFilledLevel, "RateLimitExceeded"); |
||||
|
||||
// Reduce the filledLevel and update lastUpdated |
||||
uint256 _filledLevel = adjustedFilledLevel - _newAmount; |
||||
filledLevel = _filledLevel; |
||||
lastUpdated = block.timestamp; |
||||
|
||||
return _filledLevel; |
||||
} |
||||
} |
@ -0,0 +1,159 @@ |
||||
// SPDX-License-Identifier: MIT or Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
import {Test} from "forge-std/Test.sol"; |
||||
|
||||
import {Message} from "contracts/libs/Message.sol"; |
||||
import {TokenMessage} from "contracts/token/libs/TokenMessage.sol"; |
||||
import {TypeCasts} from "contracts/libs/TypeCasts.sol"; |
||||
import {RateLimited} from "contracts/libs/RateLimited.sol"; |
||||
|
||||
import {RateLimitedHook} from "contracts/hooks/warp-route/RateLimitedHook.sol"; |
||||
import {HypERC20Collateral} from "contracts/token/HypERC20Collateral.sol"; |
||||
import {HypERC20} from "contracts/token/HypERC20.sol"; |
||||
|
||||
import {TestMailbox} from "contracts/test/TestMailbox.sol"; |
||||
import {ERC20Test} from "contracts/test/ERC20Test.sol"; |
||||
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; |
||||
|
||||
contract RateLimitedHookTest is Test { |
||||
using Message for bytes; |
||||
using TypeCasts for address; |
||||
|
||||
uint32 constant ORIGIN = 11; |
||||
uint32 constant DESTINATION = 12; |
||||
uint256 constant MAX_CAPACITY = 1 ether; |
||||
uint256 constant ONE_PERCENT = 0.01 ether; |
||||
uint8 internal constant DECIMALS = 18; |
||||
address constant BOB = address(0x2); |
||||
|
||||
TestMailbox localMailbox; |
||||
TestMailbox remoteMailbox; |
||||
ERC20Test token; |
||||
TestPostDispatchHook internal noopHook; |
||||
|
||||
RateLimitedHook rateLimitedHook; |
||||
HypERC20Collateral warpRouteLocal; |
||||
HypERC20 warpRouteRemote; |
||||
|
||||
function _mintAndApprove(uint256 amount) internal { |
||||
token.mint(amount); |
||||
token.approve(address(warpRouteLocal), amount); |
||||
} |
||||
|
||||
function setUp() external { |
||||
localMailbox = new TestMailbox(ORIGIN); |
||||
remoteMailbox = new TestMailbox(DESTINATION); |
||||
|
||||
token = new ERC20Test("Test", "Test", 100 ether, 18); |
||||
noopHook = new TestPostDispatchHook(); |
||||
rateLimitedHook = new RateLimitedHook( |
||||
address(localMailbox), |
||||
MAX_CAPACITY |
||||
); |
||||
|
||||
localMailbox.setDefaultHook(address(noopHook)); |
||||
localMailbox.setRequiredHook(address(noopHook)); |
||||
|
||||
warpRouteLocal = new HypERC20Collateral( |
||||
address(token), |
||||
address(localMailbox) |
||||
); |
||||
|
||||
warpRouteLocal.initialize( |
||||
address(rateLimitedHook), |
||||
address(0), |
||||
address(this) |
||||
); |
||||
|
||||
warpRouteRemote = new HypERC20(DECIMALS, address(remoteMailbox)); |
||||
|
||||
warpRouteLocal.enrollRemoteRouter( |
||||
DESTINATION, |
||||
address(warpRouteRemote).addressToBytes32() |
||||
); |
||||
|
||||
warpRouteRemote.enrollRemoteRouter( |
||||
ORIGIN, |
||||
address(warpRouteLocal).addressToBytes32() |
||||
); |
||||
} |
||||
|
||||
function testRateLimitedHook_revertsIfCalledByNonMailbox( |
||||
bytes calldata _message |
||||
) external { |
||||
vm.expectRevert("InvalidDispatchedMessage"); |
||||
rateLimitedHook.postDispatch(bytes(""), _message); |
||||
} |
||||
|
||||
function testRateLimitedHook_revertsTransfer_ifExceedsFilledLevel( |
||||
uint128 _amount, |
||||
uint128 _time |
||||
) external { |
||||
// Warp to a random time, get it's filled level, and try to transfer more than the target max |
||||
vm.warp(_time); |
||||
uint256 filledLevelBefore = rateLimitedHook.calculateCurrentLevel(); |
||||
vm.assume(_amount > filledLevelBefore); |
||||
_mintAndApprove(_amount); |
||||
|
||||
vm.expectRevert("RateLimitExceeded"); |
||||
warpRouteLocal.transferRemote{value: 1}( |
||||
DESTINATION, |
||||
BOB.addressToBytes32(), |
||||
_amount |
||||
); |
||||
} |
||||
|
||||
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(); |
||||
vm.assume(_amount <= filledLevelBefore); |
||||
|
||||
_mintAndApprove(_amount); |
||||
warpRouteLocal.transferRemote( |
||||
DESTINATION, |
||||
BOB.addressToBytes32(), |
||||
_amount |
||||
); |
||||
uint256 limitAfter = rateLimitedHook.calculateCurrentLevel(); |
||||
assertApproxEqRel(limitAfter, filledLevelBefore - _amount, ONE_PERCENT); |
||||
} |
||||
|
||||
function testRateLimitedHook_preventsDuplicateMessageFromValidating( |
||||
uint128 _amount |
||||
) public { |
||||
// Warp to a random time, get it's filled level, and try to transfer less than the target max |
||||
vm.warp(1 days); |
||||
uint256 filledLevelBefore = rateLimitedHook.calculateCurrentLevel(); |
||||
vm.assume(_amount <= filledLevelBefore); |
||||
|
||||
_mintAndApprove(_amount); |
||||
|
||||
// Generate an outbound message that will be the same as the one created in transferRemote |
||||
bytes memory tokenMessage = TokenMessage.format( |
||||
BOB.addressToBytes32(), |
||||
_amount, |
||||
bytes("") |
||||
); |
||||
vm.prank(address(warpRouteLocal)); |
||||
bytes memory message = localMailbox.buildOutboundMessage( |
||||
DESTINATION, |
||||
address(warpRouteRemote).addressToBytes32(), |
||||
tokenMessage |
||||
); |
||||
|
||||
bytes32 messageId = warpRouteLocal.transferRemote( |
||||
DESTINATION, |
||||
BOB.addressToBytes32(), |
||||
_amount |
||||
); |
||||
|
||||
assertEq(message.id(), messageId); |
||||
|
||||
vm.expectRevert("MessageAlreadyValidated"); |
||||
rateLimitedHook.postDispatch(bytes(""), message); |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
// SPDX-License-Identifier: MIT or Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {Test} from "forge-std/Test.sol"; |
||||
|
||||
import {TokenMessage} from "contracts/token/libs/TokenMessage.sol"; |
||||
import {TypeCasts} from "contracts/libs/TypeCasts.sol"; |
||||
|
||||
import {RateLimitedIsm} from "contracts/isms/warp-route/RateLimitedIsm.sol"; |
||||
import {TestMailbox} from "contracts/test/TestMailbox.sol"; |
||||
import {MessageUtils} from "../isms/IsmTestUtils.sol"; |
||||
import {TestRecipient} from "contracts/test/TestRecipient.sol"; |
||||
|
||||
contract RateLimitedIsmTest is Test { |
||||
using TypeCasts for address; |
||||
|
||||
uint256 MAX_CAPACITY = 1 ether; |
||||
uint32 constant ORIGIN = 11; |
||||
uint32 constant DESTINATION = 12; |
||||
address WARP_ROUTE_ADDR = makeAddr("warpRoute"); |
||||
TestMailbox localMailbox; |
||||
TestRecipient testRecipient; |
||||
RateLimitedIsm rateLimitedIsm; |
||||
|
||||
function setUp() external { |
||||
localMailbox = new TestMailbox(ORIGIN); |
||||
|
||||
rateLimitedIsm = new RateLimitedIsm( |
||||
address(localMailbox), |
||||
MAX_CAPACITY |
||||
); |
||||
testRecipient = new TestRecipient(); |
||||
|
||||
testRecipient.setInterchainSecurityModule(address(rateLimitedIsm)); |
||||
} |
||||
|
||||
function testRateLimitedIsm_revertsIDeliveredFalse( |
||||
bytes calldata _message |
||||
) external { |
||||
vm.prank(address(localMailbox)); |
||||
vm.expectRevert("InvalidDeliveredMessage"); |
||||
rateLimitedIsm.verify(bytes(""), _message); |
||||
} |
||||
|
||||
function testRateLimitedIsm_verify(uint128 _amount) external { |
||||
vm.assume(_amount <= rateLimitedIsm.calculateCurrentLevel()); |
||||
|
||||
vm.prank(address(localMailbox)); |
||||
localMailbox.process(bytes(""), _encodeTestMessage(_amount)); |
||||
} |
||||
|
||||
function testRateLimitedIsm_preventsDuplicateMessageFromValidating( |
||||
uint128 _amount |
||||
) public { |
||||
vm.assume(_amount <= rateLimitedIsm.calculateCurrentLevel()); |
||||
|
||||
bytes memory encodedMessage = _encodeTestMessage(_amount); |
||||
vm.prank(address(localMailbox)); |
||||
localMailbox.process(bytes(""), encodedMessage); |
||||
|
||||
vm.expectRevert("MessageAlreadyValidated"); |
||||
rateLimitedIsm.verify(bytes(""), encodedMessage); |
||||
} |
||||
|
||||
function _encodeTestMessage( |
||||
uint256 _amount |
||||
) internal view returns (bytes memory) { |
||||
return |
||||
MessageUtils.formatMessage( |
||||
uint8(3), |
||||
uint32(1), |
||||
ORIGIN, |
||||
WARP_ROUTE_ADDR.addressToBytes32(), |
||||
ORIGIN, |
||||
address(testRecipient).addressToBytes32(), |
||||
TokenMessage.format(bytes32(""), _amount, bytes("")) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
// SPDX-License-Identifier: MIT or Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
import {Test} from "forge-std/Test.sol"; |
||||
import {RateLimited} from "../../contracts/libs/RateLimited.sol"; |
||||
|
||||
contract RateLimitLibTest is Test { |
||||
RateLimited rateLimited; |
||||
uint256 constant MAX_CAPACITY = 1 ether; |
||||
uint256 constant ONE_PERCENT = 0.01 ether; // Used for assertApproxEqRel |
||||
address HOOK = makeAddr("HOOK"); |
||||
|
||||
function setUp() public { |
||||
rateLimited = new RateLimited(MAX_CAPACITY); |
||||
} |
||||
|
||||
function testRateLimited_setsNewLimit() external { |
||||
rateLimited.setRefillRate(2 ether); |
||||
assertApproxEqRel(rateLimited.maxCapacity(), 2 ether, ONE_PERCENT); |
||||
assertEq(rateLimited.refillRate(), uint256(2 ether) / 1 days); // 2 ether / 1 day |
||||
} |
||||
|
||||
function testRateLimited_revertsIfMaxNotSet() external { |
||||
rateLimited.setRefillRate(0); |
||||
vm.expectRevert(); |
||||
rateLimited.calculateCurrentLevel(); |
||||
} |
||||
|
||||
function testRateLimited_returnsCurrentFilledLevel_anyDay( |
||||
uint40 time |
||||
) external { |
||||
bound(time, 1 days, 2 days); |
||||
vm.warp(time); |
||||
|
||||
// Using approx because division won't be exact |
||||
assertApproxEqRel( |
||||
rateLimited.calculateCurrentLevel(), |
||||
MAX_CAPACITY, |
||||
ONE_PERCENT |
||||
); |
||||
} |
||||
|
||||
function testRateLimited_onlyOwnerCanSetTargetLimit() external { |
||||
vm.prank(address(0)); |
||||
vm.expectRevert(); |
||||
rateLimited.setRefillRate(1 ether); |
||||
} |
||||
|
||||
function testRateLimited_neverReturnsGtMaxLimit( |
||||
uint256 _newAmount, |
||||
uint40 _newTime |
||||
) external { |
||||
vm.warp(_newTime); |
||||
vm.assume(_newAmount <= rateLimited.calculateCurrentLevel()); |
||||
rateLimited.validateAndConsumeFilledLevel(_newAmount); |
||||
assertLe( |
||||
rateLimited.calculateCurrentLevel(), |
||||
rateLimited.maxCapacity() |
||||
); |
||||
} |
||||
|
||||
function testRateLimited_decreasesLimitWithinSameDay() external { |
||||
vm.warp(1 days); |
||||
uint256 currentTargetLimit = rateLimited.calculateCurrentLevel(); |
||||
uint256 amount = 0.4 ether; |
||||
uint256 newLimit = rateLimited.validateAndConsumeFilledLevel(amount); |
||||
assertEq(newLimit, currentTargetLimit - amount); |
||||
|
||||
// Consume the same amount |
||||
currentTargetLimit = rateLimited.calculateCurrentLevel(); |
||||
newLimit = rateLimited.validateAndConsumeFilledLevel(amount); |
||||
assertEq(newLimit, currentTargetLimit - amount); |
||||
|
||||
// One more to exceed limit |
||||
vm.expectRevert(); |
||||
rateLimited.validateAndConsumeFilledLevel(amount); |
||||
} |
||||
|
||||
function testRateLimited_replinishesWithinSameDay() external { |
||||
vm.warp(1 days); |
||||
uint256 amount = 0.95 ether; |
||||
uint256 newLimit = rateLimited.validateAndConsumeFilledLevel(amount); |
||||
uint256 currentTargetLimit = rateLimited.calculateCurrentLevel(); |
||||
assertApproxEqRel(currentTargetLimit, 0.05 ether, ONE_PERCENT); |
||||
|
||||
// Warp to near end-of-day |
||||
vm.warp(block.timestamp + 0.99 days); |
||||
newLimit = rateLimited.validateAndConsumeFilledLevel(amount); |
||||
assertApproxEqRel(newLimit, 0.05 ether, ONE_PERCENT); |
||||
} |
||||
|
||||
function testRateLimited_shouldResetLimit_ifDurationExceeds( |
||||
uint256 _amount |
||||
) external { |
||||
// Transfer less than the limit |
||||
vm.warp(0.5 days); |
||||
uint256 currentTargetLimit = rateLimited.calculateCurrentLevel(); |
||||
vm.assume(_amount < currentTargetLimit); |
||||
|
||||
uint256 newLimit = rateLimited.validateAndConsumeFilledLevel(_amount); |
||||
assertApproxEqRel(newLimit, currentTargetLimit - _amount, ONE_PERCENT); |
||||
|
||||
// Warp to a new cycle |
||||
vm.warp(10 days); |
||||
currentTargetLimit = rateLimited.calculateCurrentLevel(); |
||||
assertApproxEqRel(currentTargetLimit, MAX_CAPACITY, ONE_PERCENT); |
||||
} |
||||
} |
Loading…
Reference in new issue