DeFiLayer 1Layer 2
14,723 OP
View results
Submission Details
Severity: low
Valid

Profit unlocking mechanism is broken when `profit_max_unlock_time` is set to 0 since it doesn't deflates the price per share

Summary

The ScrvusdOracleV2 contract's implementation of update_profit_max_unlock_time() doesn't handle the case when setting the time to zero, unlike the original VaultV3 implementation. In VaultV3, setting this value to zero immediately unlocks all profit by burning shares and resetting variables, causing an immediate price increase whcih would cause for an inconsistent price reporting across chains wher an attacker can exploit the arbitrage since they know unlike the mainnet the price per share is not going to be updated on the destination chain.

Vulnerability Details

VaultV3::setProfitMaxUnlockTime() at https://www.contractreader.io/contract/mainnet/0x0655977FEb2f289A4aB78af67BAB0d17aAb84367

@external
def setProfitMaxUnlockTime(new_profit_max_unlock_time: uint256):
"""
@notice Set the new profit max unlock time.
@dev The time is denominated in seconds and must be less than 1 year.
We only need to update locking period if setting to 0,
since the current period will use the old rate and on the next
report it will be reset with the new unlocking time.
Setting to 0 will cause any currently locked profit to instantly
unlock and an immediate increase in the vaults Price Per Share.
@param new_profit_max_unlock_time The new profit max unlock time.
"""
self._enforce_role(msg.sender, Roles.PROFIT_UNLOCK_MANAGER)
# Must be less than one year for report cycles
assert new_profit_max_unlock_time <= 31_556_952, "profit unlock time too long"
# If setting to 0 we need to reset any locked values.
if (new_profit_max_unlock_time == 0):
share_balance: uint256 = self.balance_of[self]
if share_balance > 0:
# Burn any shares the vault still has.
self._burn_shares(share_balance, self)
# Reset unlocking variables to 0.
self.profit_unlocking_rate = 0
self.full_profit_unlock_date = 0
self.profit_max_unlock_time = new_profit_max_unlock_time
log UpdateProfitMaxUnlockTime(new_profit_max_unlock_time)

Now compare this with whats in the oracle in-scope: scrvusdOracleV2::update_profit_max_unlock_time() at ScrvusdOracleV2.vy#L333-L349

@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
"""
@notice Update price using `_parameters`
@param _profit_max_unlock_time New `profit_max_unlock_time` value
@param _block_number Block number of parameters to linearize updates
@return Boolean whether value changed
"""
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
prev_value: uint256 = self.profit_max_unlock_time
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time

We can see that the differences the VaultV3 implementation has special handling when new_profit_max_unlock_time is set to 0:

  • It burns any shares the vault holds: self._burn_shares(share_balance, self)

  • It resets unlocking variables: self.profit_unlocking_rate = 0 and self.full_profit_unlock_date = 0

  • Crucially, this causes "any currently locked profit to instantly unlock and an immediate increase in the vaults Price Per Share"

Our ScrvusdOracleV2 implementation however:

  • Does not have any special handling for when _profit_max_unlock_time is set to 0

  • Simply updates the value without any additional logic

  • Does not have the mechanisms to unlock profits by burning shares or resetting variables

This is particularly problematic because ScrvusdOracleV2 is intended to mimic the behavior of VaultV3 for cross-chain price calculations.

Impact

  • Inconsistent price calculations between chains when the profit max unlock time is set to zero since in VaultV3, setting to zero causes an immediate price increase as locked profits are released, but in ScrvusdOracleV2, no such price adjustment occurs, leading to price divergence which can be exploited for cross-chain arbitrage opportunities due to the price difference

Since the oracle is used to calculate prices across chains, this inconsistency undermines the integrity of the cross-chain price mechanisms and can create arbitrage opportunities that shouldn't exist especially when this system is then leveraged to create stableswapng-pools.

Tools Used

Manual review

Recommendations

Modify the update_profit_max_unlock_time() function in ScrvusdOracleV2 to include similar logic as VaultV3 when the profit max unlock time is set to zero:

Pseudo implementation:

@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
prev_value: uint256 = self.profit_max_unlock_time
# If setting to 0, reset unlock parameters
if (_profit_max_unlock_time == 0):
# Reset any locked shares
params: PriceParams = self._obtain_price_params(block.timestamp)
if params.balance_of_self > 0:
# Simulate burn of shares
params.total_supply -= params.balance_of_self
params.balance_of_self = 0
# Update storage with new parameters
self._update_price_params(params)
# Reset unlock rate and date
self.profit_unlocking_rate = 0
self.full_profit_unlock_date = 0
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time

This ensures that the oracle's price calculations remain consistent with the actual vault behavior when profit max unlock time is set to zero.

Updates

Lead Judging Commences

0xnevi Lead Judge
5 months ago
0xnevi Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-setProfitMaxUnlockTime-zero

Appeal created

bauchibred Submitter
5 months ago
0xnevi Lead Judge
5 months ago
0xnevi Lead Judge
4 months ago
0xnevi Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-setProfitMaxUnlockTime-zero

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.