Throttle rate will be applied incorrectly.
For instance, the RToken can be issued more than issuance throttle settings or can be redeemed less than redemption throttle settings.
Proof of Concept
RToken updates the available amount and last update time of throttle limit for issueance and redemption in issueTo, redeemTo and redeemCustom functions but not update them in mint, melt and dissolve functions.
For instance, mint function doesn't issue tokens to users but increase the totalSupply of RToken.
Since the calculation of available amount depends on totalSupply, this causes the incorrect applying of throttle rate.
Vulnerability Detail
RToken update and check the throttle in the issueTo function as follows.
function issueTo(address recipient, uint256 amount) public notIssuancePausedOrFrozen {
--- SKIP ---
uint256 supply = totalSupply();
// Revert if issuance exceeds either supply throttle
issuanceThrottle.useAvailable(supply, int256(amount)); // reverts on over-issuance
redemptionThrottle.useAvailable(supply, -int256(amount)); // shouldn't revert
--- SKIP ---
}
Then the totalSupply is used in Throttle.sol#useAvailable function as follows.
function useAvailable(
Throttle storage throttle,
uint256 supply,
int256 amount
) internal {
// untestable: amtRate will always be > 0 due to previous validations
if (throttle.params.amtRate == 0 && throttle.params.pctRate == 0) return;
// Calculate hourly limit
46: uint256 limit = hourlyLimit(throttle, supply); // {qRTok}
// Calculate available amount before supply change
49: uint256 available = currentlyAvailable(throttle, limit);
// Update throttle.timestamp if available amount changed or at limit
if (available != throttle.lastAvailable || available == limit) {
throttle.lastTimestamp = uint48(block.timestamp);
}
// Update throttle.lastAvailable
if (amount > 0) {
require(uint256(amount) <= available, "supply change throttled");
available -= uint256(amount);
// untestable: the final else statement, amount will never be 0
} else if (amount < 0) {
available += uint256(-amount);
}
throttle.lastAvailable = available;
}
totalSupply is used to calculate limit in L46 and then limit is used to calculate available in L49.
currentlyAvailable function of L49 is the following.
function currentlyAvailable(Throttle storage throttle, uint256 limit)
internal
view
returns (uint256 available)
{
74: uint48 delta = uint48(block.timestamp) - throttle.lastTimestamp; // {seconds}
75: available = throttle.lastAvailable + (limit * delta) / ONE_HOUR;
if (available > limit) available = limit;
}
As a result, we can see that the increasement of available amount depends on totalSupply and the duration of it (L74-L75).
However, mint function increase the totalSupply but doesn't update the issuanceThrottle.lastTimestamp.
From this, the following scenario is available.
Assume that issuanceThrottle.params.amtRate is zero or small, so we can ignore it.
And assume that issuanceThrottle.params.pctRate = 10%.
Assume that totalSupply of RToken is 1000 at t = 0.
Then the hourly limit of issuance is 1000 * 10% = 100.
There is no issuance of RToken for an hour.
At t = 3599, 1000 of RToken is minted by backingManager. Then totalSupply is updated to 2000 now.
At t = 3600, A user can calls issueTo to issue RToken with amount = 200.
Since totalSupply = 1000 from t = 0 to t = 3599 and totalSupply = 2000 from t = 3599 to t = 3600, the hourly limit of issuance should be (1000 * 10% * 3599 + 2000 * 10% * 1) / 3600 = 100.
However, the user issued 200 tokens in 1 hour which exceeds 100.
Lines of code
https://github.com/code-423n4/2024-07-reserve/tree/main/contracts/p1/RToken.sol#L356-L359 https://github.com/code-423n4/2024-07-reserve/tree/main/contracts/p1/RToken.sol#L370-L375 https://github.com/code-423n4/2024-07-reserve/tree/main/contracts/p1/RToken.sol#L387-L391
Vulnerability details
Impact
Throttle rate will be applied incorrectly. For instance, the
RToken
can be issued more than issuance throttle settings or can be redeemed less than redemption throttle settings.Proof of Concept
RToken
updates the available amount and last update time of throttle limit for issueance and redemption inissueTo
,redeemTo
andredeemCustom
functions but not update them inmint
,melt
anddissolve
functions. For instance,mint
function doesn't issue tokens to users but increase thetotalSupply
ofRToken
. Since the calculation of available amount depends ontotalSupply
, this causes the incorrect applying of throttle rate.Vulnerability Detail
RToken
update and check the throttle in theissueTo
function as follows.Then the
totalSupply
is used inThrottle.sol#useAvailable
function as follows.totalSupply
is used to calculatelimit
inL46
and thenlimit
is used to calculateavailable
inL49
.currentlyAvailable
function ofL49
is the following.As a result, we can see that the increasement of available amount depends on
totalSupply
and the duration of it (L74-L75
). However,mint
function increase thetotalSupply
but doesn't update theissuanceThrottle.lastTimestamp
.From this, the following scenario is available.
issuanceThrottle.params.amtRate
is zero or small, so we can ignore it. And assume thatissuanceThrottle.params.pctRate = 10%
.totalSupply
ofRToken
is1000
att = 0
. Then the hourly limit of issuance is1000 * 10% = 100
.RToken
for an hour.t = 3599
,1000
ofRToken
is minted bybackingManager
. ThentotalSupply
is updated to2000
now.t = 3600
, A user can callsissueTo
to issueRToken
withamount = 200
.totalSupply = 1000
fromt = 0
tot = 3599
andtotalSupply = 2000
fromt = 3599
tot = 3600
, the hourly limit of issuance should be(1000 * 10% * 3599 + 2000 * 10% * 1) / 3600 = 100
. However, the user issued200
tokens in1 hour
which exceeds100
.Tools Used
Manual Review
Recommended Mitigation Steps
Modify
RToken.sol#mint
function as follows.Assessed type
Math