PoolTogether - markus_ether's results

A protocol for no-loss prize savings

General Information

Platform: Code4rena

Start Date: 07/07/2023

Pot Size: $121,650 USDC

Total HM: 36

Participants: 111

Period: 7 days

Judge: Picodes

Total Solo HM: 13

Id: 258

League: ETH

PoolTogether

Findings Distribution

Researcher Performance

Rank: 26/111

Findings: 3

Award: $882.34

QA:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

2.2492 USDC - $2.25

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-396

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L394-L395

Vulnerability details

Impact

The _yieldFeeRecipient can claim some of the fees earned in the vault by calling mintYieldFee. The function has no access control so anyone can call the function and claim the yield fee for themselves.

Proof of Concept

In the below test an attacker (Alice) mints the yield fee share to an controlled address (0xf) without being authorized to call the function.


function testStealYieldFee() external {

// setup
_setLiquidationPair();
vault.setYieldFeePercentage(YIELD_FEE_PERCENTAGE);
vault.setYieldFeeRecipient(bob);
underlyingAsset.mint(address(this), 1000e18);
_sponsor(underlyingAsset, vault, 1000e18, address(this));
_accrueYield(underlyingAsset, yieldVault, 10e18);
vm.startPrank(alice);
prizeToken.mint(alice, 1000e18);
uint256 _liquidatedYield = vault.liquidatableBalanceOf(address(vault));
_liquidate(liquidationRouter, liquidationPair, prizeToken, _liquidatedYield, alice);
vm.stopPrank();

  

// pre checks
assertNotEq(vault.yieldFeeRecipient(), alice);
uint256 _yieldFeeShares = _getYieldFeeShares(_liquidatedYield, YIELD_FEE_PERCENTAGE);


// alice steal funds
vm.startPrank(alice);
vm.expectEmit();
emit MintYieldFee(alice, address(0xf), _yieldFeeShares);
vault.mintYieldFee(_yieldFeeShares, address(0xf));


// post checks
assertEq(vault.balanceOf(address(0xf)), _yieldFeeShares);
}

Tools Used

Manual review, VSCode

Add access control so that only the fee recipient can call the function:

function mintYieldFee(uint256 _shares, address _recipient) external {
_requireVaultCollateralized();
+require(msg.sender == _yieldFeeRecipient, 'Wrong caller');
}

Assessed type

Access Control

#0 - c4-judge

2023-07-14T22:14:12Z

Picodes marked the issue as primary issue

#1 - c4-judge

2023-07-14T22:17:03Z

Picodes marked issue #396 as primary and marked this issue as a duplicate of 396

#2 - c4-judge

2023-08-05T22:05:08Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: markus_ether

Also found by: josephdara

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
edited-by-warden
M-09

Awards

739.7915 USDC - $739.79

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-claimer/blob/57a381aef690a27c9198f4340747155a71cae753/src/Claimer.sol#L210 https://github.com/GenerationSoftware/pt-v5-claimer/blob/57a381aef690a27c9198f4340747155a71cae753/src/Claimer.sol#L173

Vulnerability details

Impact

PoolTogether V5 delegates the task of claiming the prizes to a network of Claimers that are incentivized to claim prizes for a share of the prize won. The fees the Claimant receives is calculated based on an Dutch Auction, but the limited by the minimum prize of the price pool

fee = min( fee in auction, % maxFeePortionOfPrize * minPrize)

When the gas costs exceed the minPrize the solver has not incentives to claim the price. Notice that the number of prizes only adjusts after very few prizes are claimed.

Proof of Concept

Gas prices rise well above the minPrize. The highest prize is suddenly drawn that day. The solvers have no incentive to claim the prize and the winner does not monitor the contract. As a result, no one claims the top prize. The protocol suffers a loss of reputation.

Tools Used

Manual review, VS Code

The problem is that the incentives are not exactly aligned. The user is willing to pay anything up to his price p to receive his price. But the maximum that the solvers receives is the minimum price. We can align the two incentives by using each user's price as an upper bound.

function _computeMaxFee(uint8 _tier, uint8 _numTiers) internal view returns (uint256) {
    uint8 _canaryTier = _numTiers - 1;
    if (_tier != _canaryTier) {
      // canary tier
-     return _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier - 1));
+	  return _computeMaxFee(prizePool.getTierPrizeSize(_tier));
	} else {
      return _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier));
    }
  }

Since we already validate the inputs in claimPrize the attacker can not claim more than the prize is worth.

Assessed type

Other

#0 - c4-judge

2023-07-16T11:11:23Z

Picodes marked the issue as primary issue

#1 - c4-sponsor

2023-07-20T22:43:50Z

asselstine marked the issue as sponsor confirmed

#2 - c4-judge

2023-08-07T16:37:57Z

Picodes marked the issue as satisfactory

#3 - c4-judge

2023-08-07T16:38:01Z

Picodes marked the issue as selected for report

#4 - asselstine

2023-08-17T21:25:11Z

Awards

140.2996 USDC - $140.30

Labels

bug
downgraded by judge
grade-a
primary issue
QA (Quality Assurance)
sponsor confirmed
edited-by-warden
Q-03

External Links

Lines of code

https://github.com/GenerationSoftware/pt-v5-claimer/blob/57a381aef690a27c9198f4340747155a71cae753/src/Claimer.sol#L137-L140

Vulnerability details

Impact

The protocol utilizes Gradual Dutch Auctions to determine the price paid to claimants. In the original paper, the number of assets owned is known in advance, enabling the auction to precisely adjust prices based on demand. However, the number of prices in the PoolTogether pool is stochastic, which can lead to pricing discrepancies when the actual number deviates significantly from the expected number.

Proof of Concept

The formula to derive the reward the claimer receives for getting an auction is

auction prize = p * e^(t - n / r)

, where t is the time passed, n is the number of claims and r is the expected number of claims per second. When the number of claims is right on target then t = n / r, so we sell at the target price p. When we have claimed a larger number of units n then expected, then t << n / r, so the prize will significantly drop below the target prize. The auction price drops below the gas fees, so many prizes will remain unclaimed.

Tools Used

Manual review, VS Code

With a large number of tiers, users and vault the probability of a large deviation is small.

For a small number of tiers, we should calculate the maximum number of prizes that will appear with a high probability (i.e. a number of draws that is not exceeded in 99.9% of the cases). We call this number the limitPrizeCount. We use the limit to estimate the maximum number of units drawn per second instead of the average number of units drawn:

SD59x18 perTimeUnit = LinearVRGDALib.getPerTimeUnit(
-    prizePool.estimatedPrizeCount(),
+    prizePool.limitPrizeCount()
     timeToReachMaxFee
    );

Assessed type

Other

#0 - c4-judge

2023-07-18T19:26:17Z

Picodes marked the issue as primary issue

#1 - asselstine

2023-07-20T22:49:54Z

Our simulator mitigates against this by having the VRGDA on a much more aggressive timeline; the duration over which prizes are auctioned is half the draw period. However, it's worth doing some additional statistical analysis to better qualify this choice.

#2 - c4-sponsor

2023-07-20T22:49:59Z

asselstine marked the issue as sponsor confirmed

#3 - c4-judge

2023-08-07T16:34:31Z

Picodes changed the severity to QA (Quality Assurance)

#4 - Picodes

2023-08-07T16:37:03Z

Downgrading to Low as despite this being a possibility, it should remain exceptional with a proper configuration, and if n >> r there may be other issues like the reserves not being enough

#5 - c4-judge

2023-08-08T14:31:26Z

Picodes marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter