Zaros allows users to deposit multiple assets as collateral that will be used for their trades.
One of these assets is Wrapped Bitcoin (WBTC) which is an ERC20 that consists in 8 decimals to represent 1 unit. This means 1e8
in balance represents 1 WBTC
.
After creating a trading account, users can deposit collateral to back their positions and start trading on Zaros, taking longs or shorts positions on the market they wish.
Once a position is opened traders are allowed to withdraw their collateral as long as they still satisfy the margin requirements of their positions after the withdrawal.
This is done using the TradingAccountBranch::withdrawMargin()
function and the margin requirement is insured by the TradingAccount::validateMarginRequirement()
internal function.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/leaves/TradingAccount.sol#L110-L127
In order to handle low decimals tokens everywhere in the protocol, Zaros starts by scaling such token amount to 18 decimals so rounding errors are avoided.
The issue arises during withdrawals because the USD price of the collateral is calculated based upon the scaled amount and not the actual amount.
The TradingAccountBranch::withdrawMargin()
function takes the following parameters :
uint128 tradingAccountId
: the user's account ID
address collateralType
: the token the user wants to withdraw
uint256 amount
: the balance
of tokens the user wants to withdraw
It starts by scaling the given amount
to 18 decimals and verifies the account holds enough collateral before subtracting it from the account storage (this collateral was also scaled to 18 decimals during deposit meaning this is compared correctly) :
After the storage has been updated, the contract will cache the different margin requirements, get the USD value of the collateral held by the account before verifying the collateral still holds the margin requirements
If we take a look at the getMarginBalanceUsd()
function, it loops through all the collateral held by the account, gets the scaled amount of the token deposited and multiplies it by its USD price and the LTV ratio.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/leaves/TradingAccount.sol#L169-L201
When dealing with low decimals assets, the adjustedBalanceUsdX18
should be down scaled to the token's real decimals number otherwise the collateral price will be way higher than it actually is.
For WBTC
since the token amount has been scaled from 8 decimals to 18, the price returned will have 10 additional decimals.
Here is a coded PoC that can be pasted in test\integration\perpetuals\perp-market-branch\getFundingRate\getFundingRate.t.sol that demonstrates a trader opening a position then withdrawing its entire collateral balance. After the operations, the position still exists and can't be liquidated even though the asset being longed crashes to 1 wei.
The estimated USD value of a collateral with low decimals is wrong.
Users are able to bypass the margin requirements during margin withdrawal using low decimals assets, leaving their position persistent and influencing the market through their contribution to the funding rate at no cost.
Moreover, the liquidation requirements rely on the same flawed metric making this kind of positions impossible to liquidate.
Manual review
Make sure the TradingAccount::getMarginBalanceUsd()
function down scales to stored collateral margin balance to the appropriate number of decimals.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.