28650 - [SC - Insight] Protocol Insolvency due to the over inflated ca...
Submitted on Feb 23rd 2024 at 01:09:56 UTC by @marqymarq10 for Boost | Puffer Finance
Report ID: #28650
Report type: Smart Contract
Report severity: Insight
Target: https://etherscan.io/address/0xd9a442856c234a39a81a089c06451ebaa4306a72
Impacts:
Protocol insolvency
Description
Brief/Intro
The protocol is at a high risk of becoming insolvent due to an error in accounting. The protocol accepts the deposit of the following three underlying assets in exchange for puffETH
:
Ether
stETH
wstETH
However, puffETH
can only be redeemed for WETH
(according to PufferVaultMainnet.sol
). The internal accounting does not take into consideration that only one asset can be redeemable for puffETH
. This leads to an over inflated perception of working capital in the protocol.
Although this issue is not fully exploitable at the moment, this bug is still within the scope of the bug bounty program because it affects assets in scope, fits the criteria of impact in scope, and the bug bounty is marked as Primacy of Impact. Additionally, this bug is directly related to concerns in the bug bounty's description:
maxWithdraw()
is in a contract that is not listed in scope as it is only overridden in PufferVaultMainnet.sol
, but it is asked to be examined. This is another reason as to why this bug remains in scope.
Vulnerability Details
As previously mentioned, the protocol accepts three underlying assets in exchange for puffETH
, but puffETH
can only be redeemed for WETH
. ERC-4626
relies on the balance of the underlying asset to calculate the value of shares. This is evident in Open Zeppelin's implementation of ERC-4626
:
Since Puffer Finance accepts three underlying assets, PufferVault::totalAssets()
has been overridden:
Additionally totalAssets()
is overridden in PufferVaultMainnet.sol
:
Regardless of which version of totalAssets()
is used the accounting error still remains. totalAssets()
is relevant due to its involvement of the calculation of maxWithdraw()
:
maxWithdraw()
calls previewRedeem()
, which in turn calls _convertToAssets()
:
As is shown above, maxUserAssets
uses the inflated totalAssets()
to calculate if a user can withdraw the underlying asset. This calculation is inherently overinflated, as the vault is accounting assets that are not truly redeemable.
In PufferVaultMainnet::withdraw()
, the asset that is meant to be sent to the user is WETH
:
This is further validated by a comment in PufferVault.sol
above totalAssets()
:
In reality, the working capital is only the assets that are held by the contract. Including accounts receivable in the calculation for working capital is misleading to users of the protocol. The over inflated value for the underlying asset will cause insolvency issues as the protocol will not hold enough of the underlying asset to fullfill their debt obligations.
Impact Details
If this accounting error is left unresolved, the users of the protocol are at risk of having their deposits stuck in the protocol indefinitely. The bug makes puffETH
an undercollateralized asset. Historically the discovery of an undercollateralized asset can lead to a bank run and a large volume of sell pressure on the open market. This would irreparably harm the reputation of Puffer Finance and along with the price of puffETH
.
References
Proof of concept
PoC
Add the following code snippet to test/integration/PufferDepositorMainnet.fork.t.sol
:
If you run the above test case, you will see the test case fails due to [FAIL. Reason: EvmError: Revert]
. If you look at the stack traces, it will show the reason for the revert as: EvmError: OutOfFund
.
Additionally, comment out PufferVaultMainnet::totalAssets()
to verify the issue still remains with PufferVault::totalAssets()
.
Last updated