Quiet Felt Orca
Medium
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.
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.
N/A
No pre-conditions need to occur except for users holding the token or protocol's integrating the token.
BoostStablecoin
'sPAUSER_ROLE
callsBoostStablecoin::pause
.
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.
// 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);
}
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.