Skip to content

Latest commit

 

History

History
240 lines (196 loc) · 8.7 KB

018.md

File metadata and controls

240 lines (196 loc) · 8.7 KB

Quiet Felt Orca

Medium

PAUSER_ROLE can cause DOS in the BoostStablecoin and any downstream protocol integrations

Summary

According the the contest README the BOOST token can be paired on any DEX within DeFi due to the free will of individuals who obtain the token.

On Line Highly centralized permissions on the BoostStablecoin allow the PAUSER_ROLE to pause all transfers which cause a DOS on the BoostStablecoin and all DeFi protocol's who integrate the token, for an indefinite amount of time.

Root Cause

  function _beforeTokenTransfer(address from, address to, uint256 amount) internal override whenNotPaused {
      super._beforeTokenTransfer(from, to, amount);
  }

The root cause lies in having the whenNotPaused modifier on the overridden _beforeTokenTransfer function in BoostStablecoin. When the contract is paused, all token transfers will revert, thus not allowing any user to perform any action on-chain with the token.

Internal pre-conditions

N/A

External pre-conditions

No pre-conditions need to occur except for users holding the token or protocol's integrating the token.

Attack Path

  1. BoostStablecoin's PAUSER_ROLE calls BoostStablecoin::pause.

Impact

Due to transfer's reverting, swaps on all DEXs will revert, bad debt could get created in lending markets, and users will not be able to redeem/sell the stablecoin for any collateral asset.

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Test.sol";
import {BoostStablecoin} from "../../contracts/BoostStablecoin.sol";
import {SolidlyV2AMO} from "../../contracts/SolidlyV2AMO.sol";
import {Minter} from "../../contracts/Minter.sol";
import {MockERC20} from "../../contracts/mock/MockERC20.sol";
import {ISolidlyRouter} from "../../contracts/interfaces/v2/ISolidlyRouter.sol";
import {IFactory} from "../../contracts/interfaces/v2/IFactory.sol";
import {IGauge} from "../../contracts/interfaces/v2/IGauge.sol";
import {IPair} from "../../contracts/interfaces/v2/IPair.sol";
import {ISolidlyV2GaugeFactory} from "./interfaces/ISolidlyV2GaugeFactory.sol";
import {ISolidlyV2BribeFactory} from "./interfaces/ISolidlyV2BribeFactory.sol";
import {Ive} from "./interfaces/Ive.sol";
import {IBribe} from "./interfaces/IBribe.sol";

import {TransparentUpgradeableProxy} from
    "../../node_modules/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract H1PocTest is Test {
    BoostStablecoin boost;
    SolidlyV2AMO solidlyV2AMO;
    Minter minter;
    MockERC20 usd;

    // Solidly v2 Contracts
    IFactory solidlyV2Factory;
    ISolidlyRouter solidlyV2Router;
    IPair solidlyV2Pair;
    ISolidlyV2GaugeFactory solidlyV2GaugeFactory;
    ISolidlyV2BribeFactory solidlyV2BribeFactory;
    IGauge solidlyV2Gauge;
    Ive ve;
    IBribe bribe;

    address admin = makeAddr("admin");
    address msig = makeAddr("msig");
    address treasury = makeAddr("treasury");
    address pauser = makeAddr("pauser");

    uint256 tokenId = 1;
    bool useTokenId = true;
    uint256 boostMultiplier = 1.1e6;
    uint24 validRangeWidth = 0.01e6;
    uint24 validRemovingRatio = 1.01e6;
    uint256 boostLowerPriceSell = 0.99e6;
    uint256 boostUpperPriceBuy = 1.01e6;
    uint256 boostSellRatio = 0.8e6;
    uint256 usdBuyRatio = 0.8e6;

    function setUp() public {
        string memory MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
        vm.createSelectFork(MAINNET_RPC_URL);
        vm.deal(admin, 1000 ether);

        usd = new MockERC20("USD", "USD", 6);

        boost = new BoostStablecoin();
        boost = BoostStablecoin(
            address(new TransparentUpgradeableProxy(address(boost), msig, abi.encodeCall(boost.initialize, (admin))))
        );
        vm.startPrank(admin);
        boost.grantRole(boost.PAUSER_ROLE(), admin);

        minter = new Minter();
        minter = Minter(
            address(
                new TransparentUpgradeableProxy(
                    address(minter),
                    msig,
                    abi.encodeCall(minter.initialize, (address(boost), address(usd), address(treasury)))
                )
            )
        );
        boost.grantRole(boost.MINTER_ROLE(), address(minter));
        boost.grantRole(boost.PAUSER_ROLE(), pauser);
        minter.grantRole(minter.MINTER_ROLE(), address(this));
        
        solidlyV2Factory = IFactory(0x777de5Fe8117cAAA7B44f396E93a401Cf5c9D4d6);
        solidlyV2Router = ISolidlyRouter(0x77784f96C936042A3ADB1dD29C91a55EB2A4219f);
        ve = Ive(0x77730ed992D286c53F3A0838232c3957dAeaaF73);
        solidlyV2BribeFactory = ISolidlyV2BribeFactory(0x2Dadc1f40eb0237BEc549862F9DEC002509ea381);
        bribe = IBribe(solidlyV2BribeFactory.createBribe());
        address pair = solidlyV2Factory.createPair(address(boost), address(usd), true);
        solidlyV2Pair = IPair(pair);

        address rewardVault = makeAddr("rewardVault");
        solidlyV2AMO = new SolidlyV2AMO();
        solidlyV2AMO = SolidlyV2AMO(
            address(
                new TransparentUpgradeableProxy(
                    address(solidlyV2AMO),
                    msig,
                    abi.encodeCall(
                        SolidlyV2AMO.initialize,
                        (
                            admin,
                            address(boost),
                            address(usd),
                            address(minter),
                            address(solidlyV2Router),
                            address(rewardVault),
                            address(rewardVault),
                            tokenId,
                            useTokenId,
                            boostMultiplier,
                            validRangeWidth,
                            validRemovingRatio,
                            boostLowerPriceSell,
                            boostUpperPriceBuy,
                            boostSellRatio,
                            usdBuyRatio
                        )
                    )
                )
            )
        );
        vm.stopPrank();
        _initLiquidity();
    }

    function testFail_BOOST_DOS() public {
        // User2 obtains BOOST stablecoin by swapping USD for BOOST within the Pool.
        address user2 = makeAddr("user2");
        usd.mint(user2, 100e6);
        vm.startPrank(user2);
        usd.approve(address(solidlyV2Router), 100e6);
        ISolidlyRouter.route[] memory path = new ISolidlyRouter.route[](1);
        path[0] = ISolidlyRouter.route({from: address(usd), to: address(boost), stable: true});

        uint256[] memory amounts = solidlyV2Router.swapExactTokensForTokens(
            100e6,
            0,
            path,
            user2,
            block.timestamp + 1 days
        );

        assertApproxEqRel(amounts[1], 100e18, 1e16);

        // Pauser pauses BOOST
        vm.startPrank(pauser);
        boost.pause();
        vm.stopPrank();

        // Establish BOOST to USD path
        ISolidlyRouter.route[] memory path2 = new ISolidlyRouter.route[](1);
        path2[0] = ISolidlyRouter.route({from: address(boost), to: address(usd), stable: true});

        // User2 tries to redeem BOOST for USD to exit the pool but cannot
        vm.startPrank(user2);
        boost.approve(address(solidlyV2Router), boost.balanceOf(user2));
        solidlyV2Router.swapExactTokensForTokens(
            boost.balanceOf(user2),
            0,
            path2,
            user2,
            block.timestamp + 1 days
        );
    }

    function _initLiquidity() internal {
        address user = makeAddr("user");

        usd.mint(user, 100_000e6);

        vm.startPrank(address(minter));
        boost.mint(user, 100_000e18);
        vm.stopPrank();

        assertEq(boost.balanceOf(user), 100_000e18);
        assertEq(usd.balanceOf(user), 100_000e6);

        // Set up the initial liquidity pool
        vm.startPrank(user);
        boost.approve(address(solidlyV2Router), 100_000e18);
        usd.approve(address(solidlyV2Router), 100_000e6);
        // Provide liquidity directly to the POOL
        (uint256 boostAmount, uint256 usdAmount, uint256 liquidity) = solidlyV2Router.addLiquidity(
            address(boost),
            address(usd),
            true,
            100_000e18,
            100_000e6,
            100_000e18,
            100_000e6,
            user,
            block.timestamp + 1 days
        );
        
        assertEq(boostAmount, 100_000e18);
        assertEq(usdAmount, 100_000e6);
    }

Mitigation

Remove the whenNotPaused modifier from BoostStablecoin::_beforeTokenTransfer. Its ok to pause things within the AMO but pausing the protocol shouldn't mean locking all users funds outside of the protocol.