Skip to content

Commit

Permalink
fix(contracts): RateLimit minor changes (hyperlane-xyz#4575)
Browse files Browse the repository at this point in the history
### Description

- Added a check for invalid capacity and event for token level change

### Drive-by changes

None

### Related issues

- fixes https://github.com/chainlight-io/2024-08-hyperlane/issues/14

### Backward compatibility

Yes

### Testing

Unit tests
  • Loading branch information
aroralanuk authored and tiendn committed Oct 25, 2024
1 parent 2b99572 commit 7aa515a
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 9 deletions.
42 changes: 34 additions & 8 deletions solidity/contracts/libs/RateLimited.sol
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/

// ============ External Imports ============
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
/// @notice Current filled level
uint256 public filledLevel;
/// @notice Tokens per second refill rate
uint256 public refillRate;
/// @notice Timestamp of the last time an action has been taken
uint256 public lastUpdated;

event RateLimitSet(uint256 _oldCapacity, uint256 _newCapacity);

event ConsumedFilledLevel(uint256 filledLevel, uint256 lastUpdated);

constructor(uint256 _capacity) {
require(
_capacity >= DURATION,
"Capacity must be greater than DURATION"
);
_transferOwnership(msg.sender);
setRefillRate(_capacity);
filledLevel = _capacity;
Expand Down Expand Up @@ -88,20 +112,22 @@ contract RateLimited is OwnableUpgradeable {

/**
* Validate an amount and decreases the currentCapacity
* @param _newAmount The amount to consume the fill level
* @param _consumedAmount The amount to consume the fill level
* @return The new filled level
*/
function validateAndConsumeFilledLevel(
uint256 _newAmount
uint256 _consumedAmount
) public returns (uint256) {
uint256 adjustedFilledLevel = calculateCurrentLevel();
require(_newAmount <= adjustedFilledLevel, "RateLimitExceeded");
require(_consumedAmount <= adjustedFilledLevel, "RateLimitExceeded");

// Reduce the filledLevel and update lastUpdated
uint256 _filledLevel = adjustedFilledLevel - _newAmount;
uint256 _filledLevel = adjustedFilledLevel - _consumedAmount;
filledLevel = _filledLevel;
lastUpdated = block.timestamp;

emit ConsumedFilledLevel(filledLevel, lastUpdated);

return _filledLevel;
}
}
47 changes: 46 additions & 1 deletion solidity/test/lib/RateLimited.t.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// 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";

Expand All @@ -13,8 +14,13 @@ contract RateLimitLibTest is Test {
rateLimited = new RateLimited(MAX_CAPACITY);
}

function testConstructor_revertsWhen_lowCapacity() public {
vm.expectRevert("Capacity must be greater than DURATION");
new RateLimited(1 days - 1);
}

function testRateLimited_setsNewLimit() external {
rateLimited.setRefillRate(2 ether);
assert(rateLimited.setRefillRate(2 ether) > 0);
assertApproxEqRel(rateLimited.maxCapacity(), 2 ether, ONE_PERCENT);
assertEq(rateLimited.refillRate(), uint256(2 ether) / 1 days); // 2 ether / 1 day
}
Expand Down Expand Up @@ -45,6 +51,25 @@ contract RateLimitLibTest is Test {
rateLimited.setRefillRate(1 ether);
}

function testConsumedFilledLevelEvent() public {
uint256 consumeAmount = 0.5 ether;

vm.expectEmit(true, true, false, true);
emit RateLimited.ConsumedFilledLevel(
499999999999993600,
block.timestamp
); // precision loss
rateLimited.validateAndConsumeFilledLevel(consumeAmount);

assertApproxEqRelDecimal(
rateLimited.filledLevel(),
MAX_CAPACITY - consumeAmount,
1e14,
0
);
assertEq(rateLimited.lastUpdated(), block.timestamp);
}

function testRateLimited_neverReturnsGtMaxLimit(
uint256 _newAmount,
uint40 _newTime
Expand Down Expand Up @@ -104,4 +129,24 @@ contract RateLimitLibTest is Test {
currentTargetLimit = rateLimited.calculateCurrentLevel();
assertApproxEqRel(currentTargetLimit, MAX_CAPACITY, ONE_PERCENT);
}

function testCalculateCurrentLevel_revertsWhenCapacityIsZero() public {
rateLimited.setRefillRate(0);

vm.expectRevert("RateLimitNotSet");
rateLimited.calculateCurrentLevel();
}

function testValidateAndConsumeFilledLevel_revertsWhenExceedingLimit()
public
{
vm.warp(1 days);
uint256 initialLevel = rateLimited.calculateCurrentLevel();

uint256 excessAmount = initialLevel + 1 ether;

vm.expectRevert("RateLimitExceeded");
rateLimited.validateAndConsumeFilledLevel(excessAmount);
assertEq(rateLimited.calculateCurrentLevel(), initialLevel);
}
}

0 comments on commit 7aa515a

Please sign in to comment.