Open code423n4 opened 2 years ago
Turbo vaults are not yield bearing because the underlying Fuse pool collaterals do not have an interest rate model. The only way to change the exchange rate is a donation, which would be profitable for users and not exploitable.
This is a difficult finding to judge as I believe there's no impact in this specific instance (per the sponsor reply)
However if we extend the finding to a broader scope (which I believe the sponsor made part of the contest when they asked for further scrutiny on the ERC4626 Mixin), there's definitely potential for concern.
I don't believe I have enough information to fully judge this finding so let me show @Joeysantoro a couple thoughts and see what we can agree on.
The Spec for ERC4626 says that totalAssets
"should include any compounding that occurs from yield", with this statement are you trying to say that:
Or are you simply saying that totalAssets can grow over time as more rewards (not counted for totalAssets) can make the pool grow?
I believe that any attempt to calculate the economical value of some pending rewards is bound to be gamed (unless the rewards farmed are in the same asset), this can be intuitively agreed on when you compare a getAmountsOut
vs actual performing a swap (which will include slippage).
As we can see in the mixin, totalAssets
is being used for minting shares, so this difference in estimates, when adding the pending reward can create opportunities for arbitrage.
Because this can lead to value extraction in a fairly reliable way:
e.g. totalAssets uses getAmountsOut
the harvest
in the strategy uses 3% slippage tolerance
This leads me to believe that there may be a medium severity finding here.
The other side of the argument is the good old "frontrunning harvest" arbitrage / exploit where any depositor could just deposit right before an harvest happens to gain access to more yield than what they contributed.
So in a way either option for totalAssets can cause issues.
Would love your thoughts here @Joeysantoro is the finding helpful in shaping the standard or the mixin? Was totalAssets
comment meant to nudge developers towards including pending rewards or not?
It should depend on whether the rewards are autocompounded in the safe. The totalAssets wouldn’t include the FEI because the FEI is not automatically converted.
A great parallel is the B Protocol BAMM on liquity. It has 3 assets, LUSD, ETH, and LQTY. The ETH is sold to autocompound LUSD but the LQTY is kept so only the ETH is counted when reporting user LUSD effective balance
Per the sponsor's reply the function totalAssets
in the mixin can be based on an exchange rate and as such can be subject to value extraction.
This is in line with a medium severity finding.
Additionally, the risk is contingent on a specific implementation, off the top of my head I'd assume that a:
Would mitigate the risk fully or at the very risk make the discrepancy between the value of the rewards and the value of the underlying more consistent.
On the other hand an implementation that is based on a single swap would be fairly practical to code but quite manipulatable to the detriment of depositors.
Notice that Yield Farming Vaults have had the issue of having harvests front-run since their inception. This is because of the fact they typically don't account for the value of pending rewards nor they use a vesting like mechanism to make the increase in harvest be fairly distributed.
The idea of totalAssets
returning the sum of the value of underlying as well as the value of rewards is in my opinion a valid attempt at trying to reduce the front-run value extraction.
However, as discussed above it can cause new types of economic exploits where based on how the value of the rewards is calculated, this value can be manipulated, tampered and ultimately exploited at the detriment of depositors.
Because this is contingent on implementation this finding cannot be of high severity.
However because economic value can be leaked, perhaps reliably, based on the specific implementation, I believe there's potential for the finding to be valid and of medium severity for the Mixin.
As in: based on the implementation of the function totalAssets
there can be MEV opportunities that could be exploited at the detriment of depositors.
However the warden submission is specific to the TurboSafe
contract, as such I'll judge the impact on that system
Because the warden made the case that the TurboSafe
logic is flawed, I'll be judging the finding based on that scope.
While my opinion is that totalAssets
can be troublesome based on implementation, I believe that in this case the impact is minimal and as the sponsor said the difference between the view function and recalculating the actual exchange rate would be meaningful exclusively if a donation where to happen.
Because of this I will downgrade the finding to Low Severity.
While in a different context there may be a risk of value extraction, I don't believe that it is the case with the Warden Provided PoC
For the sponsor, after thinking about it, I believe that the stored version can create opportunities for Value Extraction. As the Vault is quoting that price during deposit and withdrawal.
However through various operations (perhaps an external interaction directly with the cToken) the rate can be re-synched to the "real one".
I believe this can cause leak of value very reliably and as such would not recommend using this calculation in production.
For the specific case of this contest, since there's no interest rate, there's no risk.
However, using this same implementation on a yield bearing cToken can create the above mentioned side effects, I'd recommend to instead compute the latest interest rate before issuing and redeeming new shares, to avoid opportunities for value extraction
For the sake of judging the contest am rating this as a separate QA report because of the exceptionality of the finding and the thoroughness of the report, I believe 5/10 to be fair
Lines of code
https://github.com/code-423n4/2022-02-tribe-turbo/blob/66f27fe51083f49f7935e3fe594ab2380b75dee8/src/TurboSafe.sol#L151-L155 https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L131-L135 https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L137-L141 https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L143-L147 https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L149-L153
Vulnerability details
The EIP-4626 standard says that the
totalAssets()
functionSHOULD include any compounding that occurs from yield
, which means that it's normally safe for calculations involvingtotalAssets()
to use stale pricing data. However, the implementation of theERC2626
contract usestotalAssets()
in its calculations of other values.Impact
The
TurboSafe
usesassetTurboCToken.exchangeRateStored()
in its calculation oftotalAssets()
. The result of this calculation is used byERC4626.previewDeposit()
,ERC4626.previewMint()
,ERC4626.previewWithdraw()
, andERC4626.previewRedeem()
. These preview functions are called directly by the non-preview versions and therefore theTurboSafe
will use a stale (i.e. an incorrect) exchange rate leading to the misspricing of assets.Proof of Concept
Both compound-finance and Rari-Capital have the same implementations of
exchangeRateCurrent()
andexchangeRateStored()
https://github.com/compound-finance/compound-protocol/blob/ae4388e780a8d596d97619d9704a931a2752c2bc/contracts/CToken.sol#L314-L332 https://github.com/Rari-Capital/compound-protocol/blob/ae4388e780a8d596d97619d9704a931a2752c2bc/contracts/CToken.sol#L314-L332
It's used by:
https://github.com/code-423n4/2022-02-tribe-turbo/blob/66f27fe51083f49f7935e3fe594ab2380b75dee8/src/TurboSafe.sol#L151-L155
Which overrides:
https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L121
And is called by:
https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L143-L147
previewWithdraw()
is called by:https://github.com/Rari-Capital/solmate/blob/1205a9067ff957ef8b0b003ff9d77c20ef9f2e0b/src/mixins/ERC4626.sol#L74-L79
Which is called by:
https://github.com/fei-protocol/ERC4626/blob/5b786fe0317f65f5b716f577c28092fa349c4903/src/ERC4626RouterBase.sol#L40-L50
Which is called by:
https://github.com/code-423n4/2022-02-tribe-turbo/blob/66f27fe51083f49f7935e3fe594ab2380b75dee8/src/TurboRouter.sol#L94-L102
which has direct interactions with end users.
I'm not sure whether this counts as only one instance due to the
totalAssets()
call, or multiple - one for each preview function exposed to end users.Tools Used
Code inspection
Recommended Mitigation Steps
Use
assetTurboCToken.exchangeRateCurrent()
instead, or override the implementation of the non-preview versions of the functions to use a version oftotalAssets()
that usesassetTurboCToken.exchangeRateCurrent()