Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throttle::useAvailable should not update throttle.lastTimestamp if limit * delta < ONE_HOUR #22

Closed
code423n4 opened this issue Jun 9, 2023 · 5 comments
Labels
bug Something isn't working disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) downgraded by judge Judge downgraded the risk level of this issue grade-b QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax rainout Used to specify findings that came in during the rained-out audit sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/libraries/Throttle.sol#L62
https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/libraries/Throttle.sol#L73

Vulnerability details

Description

Function Throttle::currentlyAvailable calculates the current amount available for consumption. Even though the output is right, it has a problem when it is used inside Throttle::useAvailable

    function currentlyAvailable(Throttle storage throttle, uint256 limit)
        internal
        view
        returns (uint256 available)
    {
        uint48 delta = uint48(block.timestamp) - throttle.lastTimestamp; // {seconds} // @audit It uses throttle.lastTimestamp 
        available = throttle.lastAvailable + (limit * delta) / ONE_HOUR; // @audit rounding error if limit * delta < ONE_HOUR
        if (available > limit) available = limit;
    }

Considering available = throttle.lastAvailable + (limit * delta) / ONE_HOUR;, we can see there is a possible rounding error if limit * delta < ONE_HOUR, then available = throttle.lastAvailable. This rounding error is inevitable, but we must notice that delta = uint48(block.timestamp) - throttle.lastTimestamp;.

If then we consider that, inside Throttle::useAvailable the last line is throttle.lastTimestamp = uint48(block.timestamp);, we can force this rounding, DOSing the Throttle until its amount available become 0.

Impact

DOS of Throttle.lastAvailable update if limit * delta < ONE_HOUR. We can DOS this update forever if we are allowed to sent amount = 0

POC

Let's suppose we have a Throttle with next values in Ethereum network:

  • amtRate: 250
  • pctRate: 1000
  • lastTimestamp: 0
  • lastAvailable: 250

And that supply is 280_000 * 1e18

If this was the case, the limit = (supply * params.pctRate) / FIX_ONE_256 = 280_000 * 1e18 * 1000 / 1e18 = 280

Let's suppose that 1 block has passed, then block.timestamp = 12. If we call currentlyAvailable(throttle, limit):

  1. delta = block.timestamp - throttle.lastTimestamp = 12 - 0 = 12
  2. available = throttle.lastAvailable + (limit * delta) / ONE_HOUR = 1000 + (280 * 12) / 3600 = 1000 + 3360 / 3600 = 1000.

Considering amount = 1, the amount available would be updated but throttle.lastTimestamp = uint48(block.timestamp) = 12 not.

If in next block we try to do the same call, the only change of state would be lastTimestamp = uint48(block.timestamp) = 24. Meaning we can DOS lastTimestamp for 250 blocks. Which is equal to 50 minutes.

Mitigation steps

Modify Throttle::currentlyAvailable and Throttle::useAvailable in order to avoid this error. In particular I would suggest next refactor:

    function currentlyAvailable(Throttle storage throttle, uint256 limit)
        internal
        view
        returns (uint256 available, bool correspondsUpdatingLastTimestamp)
    {
        uint48 delta = uint48(block.timestamp) - throttle.lastTimestamp; // {seconds}

        uint256 accruedAmount = (limit * delta) / ONE_HOUR; 
        if(accruedAmount != 0){
            available = throttle.lastAvailable
            correspondsUpdatingLastTimestamp = true;
        }else{
            available = throttle.lastAvailable + accruedAmount;
        }

        if (available > limit) available = limit;
    }
    function useAvailable(
        Throttle storage throttle,
        uint256 supply,
        int256 amount
    ) internal {
        // untestable: amtRate will always be greater > 0 due to previous validations
        if (throttle.params.amtRate == 0 && throttle.params.pctRate == 0) return;

        // Calculate hourly limit
        uint256 limit = hourlyLimit(throttle, supply); // {qRTok}

        // Calculate available amount before supply change
        (uint256 available, bool correspondsUpdatingLastTimestamp) = currentlyAvailable(throttle, limit);

        // Calculate available amount after supply change
        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);
        }

        // Update cached values 
        throttle.lastAvailable = available;
        if(correspondsUpdatingLastTimestamp){
            throttle.lastTimestamp = uint48(block.timestamp);
        }
        
    }

Assessed type

Other

@code423n4 code423n4 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Jun 9, 2023
code423n4 added a commit that referenced this issue Jun 9, 2023
@0xean
Copy link

0xean commented Jun 11, 2023

Had a little trouble following this one, but if I am understanding this correctly, it most likely should be QA due to the nature of the rounding error and this having significantly less impact as the numbers scale.

For example, the pctRate provided isn't in the expected range of values per the docs

Default value: `2.5e16` = 2.5% per hour
Mainnet reasonable range: 1e15 to 1e18 (0.1% per hour to 100% per hour)

@itsmetechjay itsmetechjay added the rainout Used to specify findings that came in during the rained-out audit label Jun 14, 2023
@c4-sponsor
Copy link

tbrent marked the issue as disagree with severity

@c4-sponsor c4-sponsor added disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") labels Jul 4, 2023
@c4-sponsor
Copy link

tbrent marked the issue as sponsor confirmed

@tbrent
Copy link

tbrent commented Jul 4, 2023

Mitigated in reserve-protocol/protocol#857

@c4-judge c4-judge added downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Jul 12, 2023
@c4-judge
Copy link

0xean changed the severity to QA (Quality Assurance)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) downgraded by judge Judge downgraded the risk level of this issue grade-b QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax rainout Used to specify findings that came in during the rained-out audit sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
Projects
None yet
Development

No branches or pull requests

7 participants