code-423n4 / 2023-08-dopex-findings

3 stars 3 forks source link

Epoch ending timestamps are misscalculated if the duration is changed #1244

Closed code423n4 closed 1 year ago

code423n4 commented 1 year ago

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L237

Vulnerability details

Proof of Concept

The epoch in the protocol is used as the "expiry date" of options and the frequency which fundings are payed to depositors in PerpetualAtlanticVaultLP.sol. The standard epoch duration is set to 7 days in the fundingDuration state variable at PerpetualAtlanticVault.sol.

uint256 public fundingDuration = 7 days;

It can also be changed via updateFundingDuration method in PerpetualAtlanticVault.sol#L237.

  function updateFundingDuration(
    uint256 _fundingDuration
  ) external onlyRole(DEFAULT_ADMIN_ROLE) {
    fundingDuration = _fundingDuration;
  }

The function nextFundingPaymentTimestamp() calculates the ending of an epoch using the state variable fundingDuration.

  function nextFundingPaymentTimestamp()
    public
    view
    returns (uint256 timestamp)
  {
    return genesis + (latestFundingPaymentPointer * fundingDuration);
  }

Impact

First, we need to understand the function that uses the vulnerable nextFundingPaymentTimestamp and is linked to purchase and updateFundings. It's updateFundingPaymentPointer and it's purpose is to pay an epoch when it ends and also update the latestFundingPaymentPointer, which is the counter of epochs. To do that, first it checks if block.timestamp (current time) is higher or equal to nextFundingPaymentTimestamp, so it only updates if the current epoch ended. That's done in a loop way to update even if the counter is 2 epochs outdated.

  function updateFundingPaymentPointer() public {
    // 1 - LOOPS UNTIL CURRENT TIME IS LOWER THAN EPOCH ENDING
    while (block.timestamp >= nextFundingPaymentTimestamp()) {
    /// some code

    // 2 - PAY THE LAST EPOCH BEFORE STARTING A NEW ONE
            collateralToken.safeTransfer(
          addresses.perpetualAtlanticVaultLP,
          (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) /
            1e18
        );
    /// some code
    // 3 - UPDATE THE CODE TO HANDLE THE CURRENT EPOCH 
    latestFundingPaymentPointer += 1;

Impacts are scenarios of denial of service, wrong funding payments and breaking the correct calculation of nextFundingPaymentTimestamp for an unlimited amount of time:

    updateFundingPaymentPointer();

    uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer];

    uint256 startTime = lastUpdateTime == 0       ? (nextFundingPaymentTimestamp() - fundingDuration)       : lastUpdateTime;     lastUpdateTime = block.timestamp;

    collateralToken.safeTransfer(       addresses.perpetualAtlanticVaultLP,       (currentFundingRate * (block.timestamp - startTime)) / 1e18     ); // ... }

- The function pays depositors of `PerpetualAtlanticVaultLP.sol` in a drip-maner way since it's called by a lot of methods and transfers to VaultLP a quantity of funds based on how much time of the epoch has already passed `currentFundingRate * (block.timestamp - startTime)`. 
- However, now that `nextFundingPaymentTimestamp` output is bigger than it should, `updateFundingPaymentPointer` will take longer to update the epoch, so the epoch will efectively last longer.

5. The calculation `currentFundingRate * (block.timestamp - startTime)` will start to get unexpected values, since the epoch duration wasn't expected to be so big.

6. Because of (5), more funds than it should will be transfered to `VaultLP` depositors. Also, `safeTransfer` (using `SafeERC20.sol` library) will revert if `PerpetualAtlanticVault.sol` doesn't have enough funds, which probably will happen since the protocol was expecting to drip-maner pay a `8 days` epoch, not a bigger one. 

7. So, denial of service will happen and even if admin updates `updateFunding` to the standard value `7 days` again, the `nextFundingPaymentTimestamp` can have unexpected actions since his calculation is wrong and can't handle how much days passed correctly.

## Tools Used

Manual Review

## Recommended Mitigation Steps

1. Add an array to list all `fundingDuration` values used.
2. Add a mapping to map how many epochs passed for each `fundingDuration` used 
3. Make `updateFundingPaymentPointer`, which is the function that updates epochs to interact with this mapping
4. Now, `nextFundingPaymentTimestamp` can correctly calculate the end of an epoch

  function nextFundingPaymentTimestamp()     public     view     returns (uint256 timestamp)   { uint256 value; for (uint x; x < fundingDurationValues.length - 1; ++ x) { uint256 fundingDurationValue = fundingDurationValues[x]; value += epochsForEachFundingDuration[fundingDurationValue] * fundingDurationValue;     return genesis + value;   }



## Assessed type

Math
c4-pre-sort commented 1 year ago

bytes032 marked the issue as duplicate of #980

c4-pre-sort commented 1 year ago

bytes032 marked the issue as sufficient quality report

c4-judge commented 1 year ago

GalloDaSballo marked the issue as satisfactory