code-423n4 / 2023-07-pooltogether-findings

12 stars 7 forks source link

In `Vault.sol`, Math rounding is not ERC4626-complicant: `_convertToAssets()` should round up and `_convertToShares` should round down. `maxWithdraw` and `maxRedeem` does not completely adhere to EIP-4626 #250

Closed code423n4 closed 1 year ago

code423n4 commented 1 year ago

Lines of code

https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L313 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L531 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1170-L1174 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L529 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L514-L515 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L801 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1176 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d26025b4103b8d053684a75129a16852d1ae95c0/contracts/token/ERC20/extensions/ERC4626.sol#L123-L130 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L517

Vulnerability details

Impact

Anyone interacting with Pooltogether may wrongly assume that functions handle rounding according to the ERC4626 standard. Therefore, it may cause some integration problems in the future which can lead to many problems for both parties.

Proof of Concept

I) Math rounding in Vault.sol is not ERC4626-complicant: _convertToAssets() should round up and _convertToShares() should round down

File: Vault.sol
313:    uint256 _sharesToAssets = _convertToAssets(_totalShares(), Math.Rounding.Down); // @audit should be rounding up
----
531:    uint256 _assets = _convertToAssets(_shares, Math.Rounding.Down); // @audit should be rounding up
----
1170:    uint256 _totalSupplyToAssets = _convertToAssets(
1171:      _totalSupplyAmount,
1172:      _lastRecordedExchangeRate,
1173:      Math.Rounding.Down // @audit should be rounding up
1174:    );

Vault.sol#L313 , Vault.sol#L531 , Vault.sol#L1170-L1174

File: Vault.sol
517:    uint256 _shares = _convertToShares(_assets, Math.Rounding.Up); // @audit should be rounding down

Vault.sol#L517

II) maxWithdraw and maxRedeem does not completely adhere to EIP-4626

File: Vault.sol
529:    if (_shares > maxRedeem(_owner)) revert RedeemMoreThanMax(_owner, _shares, maxRedeem(_owner));
----
514:    if (_assets > maxWithdraw(_owner))
515:      revert WithdrawMoreThanMax(_owner, _assets, maxWithdraw(_owner));
----
801:    return _yieldVault.maxWithdraw(address(this)) + super.totalAssets();
----
1176:    uint256 _withdrawableAssets = _yieldVault.maxWithdraw(address(this));

The current implementations of maxWithdraw and maxRedeem do not follow this specification: LINK TO CODE

123    function maxWithdraw(address owner) public view virtual override returns (uint256) {
124:        return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
125:    }
-----
128:    function maxRedeem(address owner) public view virtual override returns (uint256) {
129:        return balanceOf(owner);
130:    }

EIP-4626 specifies concerning maxWithdraw :

MUST factor in both global and user-specific limits, like if withdrawals are entirely disabled (even temporarily) it MUST return 0.

and maxRedeem :

MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0.

The implementation does not conform to this specification. Except for first Tuesday of each month, withdraws and redeems are disabled. In such a case, these functions MUST return 0.

Tools Used

Manual Audit and EIP-4626 documentation

Recommended Mitigation Steps

Assessed type

ERC4626

c4-sponsor commented 1 year ago

asselstine marked the issue as sponsor confirmed

Picodes commented 1 year ago

" Except for first Tuesday of each month, withdraws and redeems are disabled. In such a case, these functions MUST return 0." -> I don't get this part of the report.

c4-judge commented 1 year ago

Picodes marked the issue as primary issue

c4-judge commented 1 year ago

Picodes marked the issue as satisfactory

c4-judge commented 1 year ago

Picodes marked the issue as selected for report

PierrickGT commented 1 year ago

Regarding maxWithdraw and maxRedeem, we don't have any function to stop withdrawals so these functions does not have to return 0.

About the rounding issue, EIP4626 does indeed specify that the public functions convertToAssets and convertToShares must round towards 0. Our implementation relies on OpenZeppelin implementation of the standard and is compliant with EIP4626: https://github.com/openzeppelin/openzeppelin-contracts/blob/54b3f14346da01ba0d159114b399197fea8b7cda/contracts/token/ERC20/extensions/ERC4626.sol#L104 https://github.com/openzeppelin/openzeppelin-contracts/blob/54b3f14346da01ba0d159114b399197fea8b7cda/contracts/token/ERC20/extensions/ERC4626.sol#L109

For the internal functions _convertToAssets and _convertToShares, we are also following the OpenZeppelin implementation.

For redeem: https://github.com/openzeppelin/openzeppelin-contracts/blob/54b3f14346da01ba0d159114b399197fea8b7cda/contracts/token/ERC20/extensions/ERC4626.sol#L190 https://github.com/openzeppelin/openzeppelin-contracts/blob/54b3f14346da01ba0d159114b399197fea8b7cda/contracts/token/ERC20/extensions/ERC4626.sol#L149

For withdraw: https://github.com/openzeppelin/openzeppelin-contracts/blob/54b3f14346da01ba0d159114b399197fea8b7cda/contracts/token/ERC20/extensions/ERC4626.sol#L180 https://github.com/openzeppelin/openzeppelin-contracts/blob/54b3f14346da01ba0d159114b399197fea8b7cda/contracts/token/ERC20/extensions/ERC4626.sol#L144

For availableYieldBalance and _currentExchangeRate, we are rounding down to avoid over allocating funds.

For the reasons above, I think this report is invalid.

Nadinnnn commented 1 year ago

I completely agree with the sponsor.

c4-judge commented 1 year ago

Picodes marked the issue as unsatisfactory: Invalid

Picodes commented 1 year ago

@PierrickGT nice catch. Thanks

asselstine commented 1 year ago

This one ended up in the report @Picodes , so I'll remove it