Skip to content

Latest commit

 

History

History
140 lines (98 loc) · 4.22 KB

File metadata and controls

140 lines (98 loc) · 4.22 KB

Brief Tartan Grasshopper

Medium

Distribution Creator Can Produce Insolvent Epochs

Summary

It is possible to create distributions which promise more tokens than what were deposited.

Root Cause

Precision loss in the computation of distribution settings results in marginally higher amountPerEpochs which, when aggregated, exceed the initial deposit:

uint256 fee = _amount * protocolFee / BASE_9;
uint256 realAmountToDistribute = _amount - fee;
uint256 amountPerEpoch = realAmountToDistribute / ((_endBlockNum - _startBlockNum) / blocksPerEpoch);

https://github.com/sherlock-audit/2024-10-gamma-rewarder/blob/475f7fbd0f7c2717ed585a67632e9a675b51c306/GammaRewarder/contracts/GammaRewarder.sol#L125C8-L127C110

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

No response

Impact

The protocol takes on atomic bad debt through the commitment to pay at a marginally higher rate.

PoC

The following fuzz test can discover realistic distribution settings which result in marginally insolvent distributions.

// SPDX-License-Identifier: CC0
pragma solidity 0.8.13;

import {Test, console} from "forge-std/Test.sol";

contract PrecisionLossTest is Test {

  uint256 public constant BASE_9 = 1e9;
  uint256 public MAX_DISTRIBUTION_BLOCKS = 9_676_800;

  function testFuzz_precisionLoss(
    uint256 _amount,
    uint256 protocolFee,
    uint256 blocksPerEpoch,
    uint256 _endBlockNum,
    uint256 _startBlockNum
  ) external {
    _testFuzz_precisionLoss(_amount, protocolFee, blocksPerEpoch, _endBlockNum, _startBlockNum);
  }

  function _testFuzz_precisionLoss(
    uint256 _amount,
    uint256 protocolFee,
    uint256 blocksPerEpoch,
    uint256 _endBlockNum,
    uint256 _startBlockNum
  ) internal {
    {
      bool invalidAmount = _amount < 1000e6 || _amount > 1000 ether;
      bool invalidBlocksPerEpoch = blocksPerEpoch < 1000 || blocksPerEpoch > MAX_DISTRIBUTION_BLOCKS;
      bool invalidProtocolFee = protocolFee > BASE_9;
      bool invalidBlocks = _endBlockNum <= _startBlockNum;

      if (invalidBlocks || invalidBlocksPerEpoch) return;
      vm.assume(!invalidBlocks && !invalidBlocksPerEpoch);

      bool invalidBlocksEpoch = ((_endBlockNum - _startBlockNum) % blocksPerEpoch) != 0;
      bool invalidDivisor = blocksPerEpoch > _endBlockNum - _startBlockNum;

      if (
          invalidAmount
       || invalidProtocolFee
       || invalidBlocksEpoch
       || invalidDivisor
      ) return;

      vm.assume(
           !invalidAmount
        && !invalidBlocksPerEpoch
        && !invalidProtocolFee
        && !invalidBlocks
        && !invalidBlocksEpoch
        && !invalidDivisor
      );
    }

    uint256 fee = _amount * protocolFee / BASE_9;
    uint256 _realAmountToDistribute = _amount - fee;
    uint256 amountPerEpoch = _realAmountToDistribute / ((_endBlockNum - _startBlockNum) / blocksPerEpoch);

    uint256 numberOfBlocks = _endBlockNum - _startBlockNum;
    uint256 numberOfEpochs = numberOfBlocks / blocksPerEpoch;

    assert((amountPerEpoch * numberOfEpochs) == _realAmountToDistribute);

  }
    
}

Running:

forge test --match-test "testFuzz_precisionLoss"

Yields:

[FAIL. Reason: panic: assertion failed (0x01); counterexample: calldata=0x2514078000000000000000000000000000000000000000000000000000000000ba414fa7000000000000000000000000000000000000000000000000000000002ade387f00000000000000000000000000000000000000000000000000000000000005bd000000000000000000000000000000000000000000000000000000003e5e3c22000000000000000000000000000000000000000000000000000000000000015a args=[3124842407 [3.124e9], 719206527 [7.192e8], 1469, 1046363170 [1.046e9], 346]] testFuzz_precisionLoss(uint256,uint256,uint256,uint256,uint256) (runs: 177485, μ: 2678, ~: 2673)

Extrapolating the fuzz parameters, we equate to:

uint256 _amount = 3124842407;
uint256 protocolFee = 719206527;
uint256 blocksPerEpoch = 1469;
uint256 _endBlockNum = 1046363170;
uint256 _startBlockNum = 346;

These resolve to an amountPerEpoch * numberOfEpochs of 876836376, versus a _realAmountToDistribute of 877435353.

Mitigation

Do not calculate the amountPerEpoch, instead have the caller specify it via function parameter and evaluate the total cost.