Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $36,500 USDC
Total HM: 9
Participants: 80
Period: 7 days
Judge: hansfriese
Total Solo HM: 2
Id: 332
League: ETH
Rank: 4/80
Findings: 2
Award: $1,189.63
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: DarkTower
Also found by: 0xJaeger, 0xJoyBoy03, 0xRiO, 0xkeesmark, 0xlemon, 0xmystery, Abdessamed, AcT3R, Afriauditor, AgileJune, Al-Qa-qa, Aymen0909, Daniel526, DanielTan_MetaTrust, Dots, FastChecker, Fitro, GoSlang, Greed, Krace, McToady, SoosheeTheWise, Tripathi, asui, aua_oo7, btk, crypticdefense, d3e4, dd0x7e8, dvrkzy, gesha17, iberry, kR1s, leegh, marqymarq10, n1punp, pa6kuda, radin100, sammy, smbv-1923, trachev, turvy_fuzz, valentin_s2304, wangxx2026, y4y, yotov721, yvuchev, zhaojie
1.4652 USDC - $1.47
Calling PrizeVault.claimYieldFeeShares(uint256 _shares)
with any amount of _shares
(that passes function conditions) will result in loss of yield (loss of yieldFeeBalance
inside vault) because it resets yieldFeeBalance
. When yieldFeeRecipient calls this function with _shares
that is less then _yieldFeeBalance
and not equal to 0, it will lead to _yieldFeeBalance - shares
amount of yield being stuck in the PrizeVault
contact.
Fees are stuck in PrizeVault
contract.
Add this test to PrizeVault.t.sol
and run with
forge test --match-contract PrizeVaultTest --match-test testClaimYieldFeeShares_LossOfFundsIfClaimNotAllYieldFeeBalance
function testClaimYieldFeeShares_LossOfFundsIfClaimNotAllYieldFeeBalance() public { vault.setYieldFeePercentage(1e8); // 10% vault.setYieldFeeRecipient(bob); assertEq(vault.totalDebt(), 0); // make an initial deposit underlyingAsset.mint(alice, 1e18); vm.startPrank(alice); underlyingAsset.approve(address(vault), 1e18); vault.deposit(1e18, alice); vm.stopPrank(); underlyingAsset.mint(address(vault), 1e18); vault.setLiquidationPair(address(this)); uint256 maxLiquidation = vault.liquidatableBalanceOf(address(underlyingAsset)); uint256 amountOut = maxLiquidation / 2; uint256 yieldFee = (1e18 - vault.yieldBuffer()) / (2 * 10); vault.transferTokensOut(address(0), bob, address(underlyingAsset), amountOut); vm.prank(bob); // claim half of yieldFee vault.claimYieldFeeShares(yieldFee / 2); // because claimYieldFeeShares resets yieldFeeBalance it will always be 0 not depending on the amount of _shares assertEq(vault.yieldFeeBalance(), 0); }
Substruct _shares
instead of _yieldFeeBalance
in PrizeVault.claimYieldFeeShares()
.
function claimYieldFeeShares(uint256 _shares) external onlyYieldFeeRecipient { if (_shares == 0) revert MintZeroShares(); uint256 _yieldFeeBalance = yieldFeeBalance; if (_shares > _yieldFeeBalance) revert SharesExceedsYieldFeeBalance(_shares, _yieldFeeBalance); - yieldFeeBalance -= _yieldFeeBalance; + yieldFeeBalance -= _shares; _mint(msg.sender, _shares); emit ClaimYieldFeeShares(msg.sender, _shares); }
Math
#0 - c4-pre-sort
2024-03-11T21:36:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-11T21:36:48Z
raymondfam marked the issue as duplicate of #10
#2 - c4-pre-sort
2024-03-13T04:38:04Z
raymondfam marked the issue as duplicate of #59
#3 - c4-judge
2024-03-15T07:40:43Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: pa6kuda
Also found by: 0xhunter20, Afriauditor
1188.1588 USDC - $1,188.16
https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L368-L392 https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L685 https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L619
Current PrizeVault.maxDeposit()
calculates the maximum possible amount of deposit without taking into account produced fees. That means if there is already maxed deposited amount of asset that is calculated by the current implementation if PrizeVault.maxDeposit()
yieldFeeRecipient
can't withdraw shares with PrizeVault.claimYieldFeeShares()
because in that case _mint()
will revert because of overflow. A lot of low-price tokens can exceed the limit of type(uint96).max
with ease. For example to make a deposit with maxDeposit()
value with LADYS token it's needed only $13568 (as of 08.03.2024).
If a user makes a maximum allowed deposit that is calculated by the current implementation of PrizeVault.maxDeposit()
yieldFeeRecipient
can't withdraw fees if they are available.
Add this test to PrizeVault.t.sol
and run with
forge test --match-contract PrizeVaultTest --match-test testMaxDeposit_CalculatesWithoutTakingIntoAccountGeneratedFees
function _deposit(address account, uint256 amount) private { underlyingAsset.mint(account, amount); vm.startPrank(account); underlyingAsset.approve(address(vault), amount); vault.deposit(amount, account); vm.stopPrank(); } function testMaxDeposit_CalculatesWithoutTakingIntoAccountGeneratedFees() public { vault.setYieldFeePercentage(1e8); // 10% vault.setYieldFeeRecipient(bob); // alice make initial deposit _deposit(alice, 1e18); // mint yield to the vault and liquidate underlyingAsset.mint(address(vault), 1e18); vault.setLiquidationPair(address(this)); uint256 maxLiquidation = vault.liquidatableBalanceOf(address(underlyingAsset)); uint256 amountOut = maxLiquidation / 2; uint256 yieldFee = (1e18 - vault.yieldBuffer()) / (2 * 10); // 10% yield fee + 90% amountOut = 100% // bob transfers tokens out and increase fee vault.transferTokensOut(address(0), bob, address(underlyingAsset), amountOut); // alice make deposit with maximum available value for deposit uint256 maxDeposit = vault.maxDeposit(address(this)); _deposit(alice, maxDeposit); // then bob want to withdraw earned fee but he can't do that vm.prank(bob); vm.expectRevert(); vault.claimYieldFeeShares(yieldFee); }
Add function to withdraw fees in asset
.
Or change function PrizeVault.maxDeposit()
to calculate max deposit with taking into account produced fees:
function maxDeposit(address) public view returns (uint256) { uint256 _totalSupply = totalSupply(); uint256 totalDebt_ = _totalDebt(_totalSupply); if (totalAssets() < totalDebt_) return 0; // the vault will never mint more than 1 share per asset, so no need to convert supply limit to assets uint256 twabSupplyLimit_ = _twabSupplyLimit(_totalSupply); uint256 _maxDeposit; uint256 _latentBalance = _asset.balanceOf(address(this)); uint256 _maxYieldVaultDeposit = yieldVault.maxDeposit(address(this)); if (_latentBalance >= _maxYieldVaultDeposit) { return 0; } else { unchecked { _maxDeposit = _maxYieldVaultDeposit - _latentBalance; } - return twabSupplyLimit_ < _maxDeposit ? twabSupplyLimit_ : _maxDeposit; + return twabSupplyLimit_ < _maxDeposit ? twabSupplyLimit_ - yieldFeeBalance : _maxDeposit - yieldFeeBalance; } }
Math
#0 - c4-pre-sort
2024-03-12T05:21:52Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-03-12T05:22:36Z
raymondfam marked the issue as duplicate of #64
#2 - c4-judge
2024-03-18T01:55:20Z
hansfriese marked the issue as not a duplicate
#3 - c4-judge
2024-03-18T01:56:55Z
hansfriese marked the issue as primary issue
#4 - c4-judge
2024-03-18T01:58:25Z
hansfriese marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-03-18T02:13:23Z
hansfriese marked the issue as satisfactory
#6 - c4-sponsor
2024-03-18T14:34:42Z
trmid (sponsor) acknowledged
#7 - c4-judge
2024-03-18T14:41:47Z
hansfriese marked the issue as selected for report
#8 - c4-sponsor
2024-03-18T14:44:01Z
trmid (sponsor) confirmed
#9 - trmid
2024-03-18T14:49:51Z
The TWAB max supply limit is a known issue with the prize vault and the deployer is expected to evaluate the possibility of the limit being exceeded before deploying a new prize vault. This issue has demonstrated that any yield fees accrued in this state may end up locked in the prize vault until enough withdrawals occur to free up the TWAB supply limit. This is undesirable behaviour since all funds that have entered the prize vault through deposits or yield should be able to be taken out in these unexpected circumstances.
#10 - trmid
2024-03-20T02:15:03Z