Closed code423n4 closed 2 years ago
Similar to my issue (#144) but goes into more detail. (Nice work xiaming90!)
Great detail. In the end this appears to be the intended design - the concern and recommendations here are considerations to potentially improve the user experience. Merging with the warden's QA report #209
Lines of code
https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L413 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Twav/Twav.sol#L35
Vulnerability details
Vulnerability Details
It was observed that the buyout rejection feature is not deterministic and does not always work as intented. The outcome of the buyout rejection depends on a number of external factors and actors, apart from the
buyoutRejectionValuation
and current valuation.Per the specification,
buyoutRejectionValuation
is calculated asbuyoutBid * (1 + buyoutRejectionPremium)
. If the time weighted average valuation (TWAV) becomes greater than or equal tobuyoutRejectionValuation
within 5 days, the buyout will be rejected. However, due to the current implementation of the TWAV, the buyout rejection does not always work as intented even though the current valuation is greater than or equal tobuyoutRejectionValuation
, because it depends on many external factors such as how often the_updateTwav
function is triggered or when the_updateTwav
is triggered.Proof-of-Concept
Assume that the buyout price is 100 ETH, and the
buyoutRejectionValuation
is 120 ETH, and theBUYOUT_DURATION
= 5 days.Charles has initiated a buyout by calling the
initiateBuyout
function. When theinitiateBuyout
function is triggered, the_updateTWAV
will be called and the current valuation will be added to the first position (= zero) of thetwavObservations
array. The second, third, and forth positions of thetwavObservations
array are empty and uninitialised at the moment. Following is the visualisation of thetwavObservation
array at this point:Another important point to note is that the
getTwav
will returns 0 untiltwavObservations
array is filled up or full.https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Twav/Twav.sol#L35
The following show two scenarios where the buyout rejection does not work as intented:
Scenario #1
Alice wants to reject Charles's buyout by calling the
buy()
function. Thus, she brought large number of factionalized tokens from the vault to push the curent valuation to 150 ETH on Day 0 shortly after the buyout is initiated by Charles. Following is the visualisation of thetwavObservation
array at this point:Assume that from this point onwards until the end of the buyout period at Day 5, no one buy and sell any fractionalized token or call the
Nibbl.updateTWAV()
function. As a result, thetwavObservation
array remains the same.At the end of the buyout period at Day 5, Charles successfully buyout the vault/NFT since he was not rejected by the system.
Even though the current valuation is 150 ETH, which is higher than the
buyoutRejectionValuation
(120 ETH), Charles still managed to buyout the NFT/vault. Per the specification, Charles's buyout should be rejected.Scenario #2
Alice wants to reject Charles's buyout by calling the
buy()
function. Thus, she brought large number of factionalized tokens from the vault to push the curent valuation to 150 ETH on Day 0 shortly after the buyout is initiated by Charles. Following is the visualisation of thetwavObservation
array at this point:Someone buys some factionalized tokens by calling the
buy()
function and the_updateTWAV
is triggered. Therefore, the current valuation is added to the 3rd position of thetwavObservation
array. Following is the visualisation of thetwavObservation
array at this point:Next,
_rejectBuyout()
function will be triggered. However, since thetwavObservations
array is not filled up yet, it will return zero. Thus, the buyout is not rejected.Assume that from this point onwards until the end of the buyout period at Day 5, no one buy and sell any fractionalized token or call the
Nibbl.updateTWAV()
function. As a result, thetwavObservation
array remains the same.At the end of the buyout period at Day 5, Charles successfully buyout the vault/NFT since he was not rejected by the system
Even though the current valuation is 150 ETH, which is higher than the
buyoutRejectionValuation
(120 ETH), Charles still managed to buyout the NFT/vault. Per the specification, Charles's buyout should be rejected.Remarks About Existing TWAP Implementation
There is a seperate issue about the existing TWAP implementation, thus refer to my another report for detailed write-up.
In summary, the report highlighted that the current TWAP implementation is not working as intended. If the TWAP is properly implemented and the TWAP's windows/time period is 1 hour, the current valuation of 150 ETH will be returned by the TWAP shortly after 1 hour after Alice has initiated the buy order. Even if there is no transaction happening between Day 0 and Day 5 after Alice's last transaction, the TWAP will still return 150 ETH on Day 5 and cause the buyout to reject. As such, the buyout would not succeed. However, it is not the case in the current implementation.
Impact
Recommended Mitigation Steps
There are a number of design issues that cause this issue:
twavObservation
array depends on user activities to be filled up or populated. If there is no user activities at all, thetwavObservation
array will be stuck in the same state. The protocol optimistically assumes that there will always be sufficient user activities (users callingbuy
andsell
) to fill up thetwavObservation
array and keep the TWAP up-to-date.twavObservation
array is only initialised and being utilised after the buyout has been triggered. Additionally, it needs to wait for at least four (4) valuations to fill up thetwavObservations
array before thegetTwav
starts to return the valuation. Thus, it introduces some latency.getTwav
will returns zero (0) untiltwavObservations
array is filled up or full. However, note that there is a chance that there is no user activities to fill up thetwavObservations
array. In this case,getTwav
will always return zero (0).Consider the following measures the mitigate the issue:
TWAV
value should be 150 ETH, and the buyout will be rejected eventually.buy
orsell
even if the vault is not in buyout state. In short, track or update the TWAP all the time whenever there is a state change to the current valuationbuyoutTime
has passed to determine if the buyout is successful. The system should check if the current valuation is higher than thebuyoutRejectionValuation
at the end of the buyout period before deciding if the buyout is successful or not.