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

0xPhantom - DOS in updateRatioForReward in the GoodDollarExchangeProvider #66

Open
sherlock-admin4 opened this issue Oct 31, 2024 · 0 comments

Comments

@sherlock-admin4
Copy link
Contributor

sherlock-admin4 commented Oct 31, 2024

0xPhantom

High

DOS in updateRatioForReward in the GoodDollarExchangeProvider

Summary

In updateRatioForReward, the newRatioUint can round down to zero, which will lead to a DOS in updateRatioForReward and currentPrice, as well as total miscalculations in the protocol.

https://github.com/sherlock-audit/2024-10-mento-update/blob/main/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol#L203

Root Cause

We can see here that if the reward and the total supply are too high compared to the reserve balance – which is likely because the reserve asset is meant to be the cUSD and the current price of the G$ is $0.00003726 – the newRatioScaled will be less than 1e10, leading to a reserve ratio of 0, which will DOS the updateReward function.

 function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused {
    PoolExchange memory exchange = getPoolExchange(exchangeId);

    uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset];
    uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress];

    UD60x18 numerator = wrap(exchange.reserveBalance);
    UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled));
    uint256 newRatioScaled = unwrap(numerator.div(denominator));

    uint32 newRatioUint = uint32(newRatioScaled / 1e10);
    exchanges[exchangeId].reserveRatio = newRatioUint;
    exchanges[exchangeId].tokenSupply += rewardScaled;

    emit ReserveRatioUpdated(exchangeId, newRatioUint);
  }

This is because it uses the function currentPrice, which will be DOS due to the denominator being equal to 0. The call will revert due to a division or modulo by zero.

  function currentPrice(bytes32 exchangeId) public view returns (uint256 price) {
    // calculates: reserveBalance / (tokenSupply * reserveRatio)
    require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist");
    PoolExchange memory exchange = getPoolExchange(exchangeId);
    uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10;
    UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio));
    price = unwrap(wrap(exchange.reserveBalance).div(denominator));
    return price;
  }

Internal pre-conditions

1 the totalSupply + the reward * currentPrice > reserveBalance enough to a round down to 0.

External pre-conditions

None.

Attack Path

  1. the avatar calls mintRewardFromReserveRatio make the rewardRatio round down to Zero

Impact

The calculation of the protocol is broke without a rewardRatio and the updateRatioForReward function is forever DOS no reward will ever be minted.

PoC

You can copy paste this code in a new file in the test folder and then run forge test --mt test_test_mintRewardRatioPOC

// SPDX-License-Identifier: GPL-2.0
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {ERC20} from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol";
import {BancorExchangeProvider} from "contracts/goodDollar/BancorExchangeProvider.sol";
import {GoodDollarExchangeProvider} from "contracts/goodDollar/GoodDollarExchangeProvider.sol";
import {IReserve} from "contracts/interfaces/IReserve.sol";
import {IExchangeProvider} from "contracts/interfaces/IExchangeProvider.sol";
import {ERC20Mock} from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol";
import {GoodDollarExpansionController} from "contracts/goodDollar/GoodDollarExpansionController.sol";
import {MockReserve} from "test/utils/mocks/MockReserve.sol";
import {IGoodDollarExpansionController} from "contracts/interfaces/IGoodDollarExpansionController.sol";
import {IGoodDollarExchangeProvider} from "contracts/interfaces/IGoodDollarExchangeProvider.sol";
import {IBancorExchangeProvider} from "contracts/interfaces/IBancorExchangeProvider.sol";
import {IDistributionHelper} from "contracts/goodDollar/interfaces/IGoodProtocol.sol";
import {Broker} from "contracts/swap/Broker.sol";
import {GoodDollarExpansionControllerHarness} from "test/utils/harnesses/GoodDollarExpansionControllerHarness.sol";
import {TestERC20} from "test/utils/mocks/TestERC20.sol";

contract CodedPOC is Test  {
 ERC20Mock reserveToken;
    TestERC20 token;
    TestERC20 token2;
    address reserveAddress;
    address brokerAddress = vm.addr(116);
    address avatarAddress = vm.addr(119);
    address expansionControllerAddress = vm.addr(121);
    address distributionHelper ;

    IBancorExchangeProvider.PoolExchange poolExchange1;
    IBancorExchangeProvider.PoolExchange poolExchange2;
    BancorExchangeProvider bancorExchangeProvider;
    GoodDollarExchangeProvider goodDollarExchangeProvider;
    MockReserve mockReserve;
    MockReserve mockReserve1;

    GoodDollarExpansionController expansionController;
    Broker broker;
    address[] exchangeProviders;
    address[] reserves;
    address constant USER1 = address(0x10000);
    address constant USER2 = address(0x20000);
    address constant USER3 = address(0x30000);
    address[] internal users;
    address sender;
    bytes32 exchangeIdGoodDollar1;
    bytes32 exchangeIdGoodDollar2;
    bytes32 exchangeIdBancor1;
    bytes32 exchangeIdBancor2;
    event LogBytes32(string message , bytes32 value);
    event LogUint256(string message , uint256 value);
    bytes32 exchangeId;
    bytes32 exchangeId2;
     uint64 expansionRate = 1e18 * 0.01;
    uint32 expansionFrequency = uint32(1 days);
    mapping(bytes32 => IBancorExchangeProvider.PoolExchange) fromIdToConfig;
    function setUp() public  {
        reserveToken = new ERC20Mock("cUSD", "cUSD", address(this), 4_000_000_000e18);
        token = new TestERC20("Good$", "G$");
        token2 = new TestERC20("Good2$", "G2$");
        users = [USER1, USER2, USER3];

        poolExchange1 = IBancorExchangeProvider.PoolExchange({
            reserveAsset: address(reserveToken),
            tokenAddress: address(token),
            tokenSupply: 300_000 * 1e18,
            reserveBalance: 60_000 * 1e18,
            reserveRatio: 1e8 * 0.2,
            exitContribution: 1e8 * 0.01
        });

        poolExchange2 = IBancorExchangeProvider.PoolExchange({
            reserveAsset: address(reserveToken),
            tokenAddress: address(token2),
            tokenSupply: 300_000 * 1e18,
            reserveBalance: 60_000 * 1e18,
            reserveRatio: 1e8 * 0.2,
            exitContribution: 1e8 * 0.01
        });
        
        mockReserve = new MockReserve();
        mockReserve1 = new MockReserve();
        reserveAddress = address(mockReserve);
        mockReserve.addCollateralAsset(address(reserveToken));
        mockReserve.addToken(address(token));
        mockReserve.addToken(address(token2));
        bancorExchangeProvider = initializeBancorExchangeProvider();
        expansionController = new GoodDollarExpansionController(false);
        goodDollarExchangeProvider = new GoodDollarExchangeProvider(false);
        distributionHelper = address(new DistributionHelper());
        expansionController.initialize(
            address(goodDollarExchangeProvider), address(distributionHelper), address(mockReserve), address(avatarAddress)
        );
        broker = new Broker(true);
        goodDollarExchangeProvider.initialize(
            address(broker), address(mockReserve), address(expansionController), address(avatarAddress)
        );
        exchangeProviders.push(address(goodDollarExchangeProvider));
        exchangeProviders.push(address(bancorExchangeProvider));
        reserves.push(address(mockReserve));
        reserves.push(address(mockReserve1));
        broker.initialize(exchangeProviders, reserves);
          for (uint256 i = 0; i < users.length; i++) {
            reserveToken.transfer(users[i], 1_000_000_000e18);
            vm.startPrank(users[i]);
            reserveToken.approve(address(broker), type(uint256).max);
            token.approve(address(broker), type(uint256).max);
            token2.approve(address(broker), type(uint256).max);
            reserveToken.approve(address(expansionController), type(uint256).max);
            token.approve(address(expansionController), type(uint256).max);
            token2.approve(address(expansionController), type(uint256).max);
            vm.stopPrank();
        }
        exchangeId= keccak256(abi.encodePacked(reserveToken.symbol(), token.symbol()));
        exchangeId2= keccak256(abi.encodePacked(reserveToken.symbol(), token2.symbol()));
        vm.label(address(token), "Good$");
        vm.label(address(token2), "Good2$");
    }
 function initializeBancorExchangeProvider() internal returns (BancorExchangeProvider) {
        BancorExchangeProvider bancorExchangeProviderIntern = new BancorExchangeProvider(false);

        bancorExchangeProviderIntern.initialize(brokerAddress, reserveAddress);
        return bancorExchangeProviderIntern;
    }
 function test_test_mintRewardRatioPOC() public {
            vm.prank(avatarAddress);
       goodDollarExchangeProvider.createExchange(IBancorExchangeProvider.PoolExchange({ reserveAsset: address(reserveToken), tokenAddress: address(token), tokenSupply: 1e18 , reserveBalance: 1e18 , reserveRatio: 0.1e8, exitContribution: 0 }));
       vm.prank(avatarAddress);
       expansionController.mintRewardFromReserveRatio(exchangeId, USER1, 1.001e25 );
       vm.prank(avatarAddress);
       expansionController.mintRewardFromReserveRatio(exchangeId, USER1, 1);
    }
}
contract DistributionHelper {
     function onDistribution(uint256 _amount) external{}
}

You should have an output like that :

Ran 1 test for test/CodedPOC.sol:CodedPOC
[FAIL: panic: division or modulo by zero (0x12)] test_test_mintRewardRatioPOC() (gas: 372309)

Mitigation

In my opinion you could add some precision to the rateRatio or add a sefety check in the updateRatioForReward Function like this :

 function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused {
    PoolExchange memory exchange = getPoolExchange(exchangeId);

    uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset];
    uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress];

    UD60x18 numerator = wrap(exchange.reserveBalance);
    UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled));
    uint256 newRatioScaled = unwrap(numerator.div(denominator));

    uint32 newRatioUint = uint32(newRatioScaled / 1e10);
     require(newRatioUint!=0, "ratio limit");
    exchanges[exchangeId].reserveRatio = newRatioUint;
    exchanges[exchangeId].tokenSupply += rewardScaled;

    emit ReserveRatioUpdated(exchangeId, newRatioUint);
  }
@sherlock-admin3 sherlock-admin3 changed the title Wide Pickle Crane - DOS in updateRatioForReward in the GoodDollarExchangeProvider 0xPhantom - DOS in updateRatioForReward in the GoodDollarExchangeProvider Nov 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant