PoolTogether - pa6kuda's results

General Information

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

PoolTogether

Findings Distribution

Researcher Performance

Rank: 4/80

Findings: 2

Award: $1,189.63

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

1.4652 USDC - $1.47

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
edited-by-warden
:robot:_10_group
duplicate-59

External Links

Lines of code

https://github.com/code-423n4/2024-03-pooltogether/blob/480d58b9e8611c13587f28811864aea138a0021a/pt-v5-vault/src/PrizeVault.sol#L617

Vulnerability details

Summary

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.

Impact

Fees are stuck in PrizeVault contract.

Proof of Concept

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);
    }

Assessed type

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

Findings Information

🌟 Selected for report: pa6kuda

Also found by: 0xhunter20, Afriauditor

Labels

bug
2 (Med Risk)
insufficient quality report
primary issue
satisfactory
selected for report
sponsor confirmed
:robot:_66_group
M-07

Awards

1188.1588 USDC - $1,188.16

External Links

Lines of code

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

Vulnerability details

Summary

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).

Impact

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.

Proof of Concept

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;
        }
    }

Assessed type

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

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