Guardian's review of Treasury Market for Synthetix, published February 2025. The report records 45 findings across 2 review rounds, including 5 critical and 7 high.
- Published
- Review window
- January 29 to February 11, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum, Optimism, Base, Arbitrum
- Sector
- Perpetuals
- 5 Critical
- 7 High
- 13 Medium
- 20 Low
- 0 Informational
Scope
3 files in scope · 297 nSLOC
| File | nSLOC | Lines |
|---|---|---|
markets/treasury-market/contracts/TreasuryMarket.sol | 285 | 434 |
markets/treasury-market/contracts/SynthetixTreasuryProxy.sol | 8 | 17 |
markets/treasury-market/contracts/InitialModuleBundle.sol | 4 | 12 |
Findings 45
Main Review
31 findings · January 29, 2025-
C-01 Critical New Pool Debt Distribution Not Updated Logical Error Acknowledged
Description
In the
migrateDelegationfunction there is noupdateAccountDebtinvocation for the new pool, therefore the pending debt distribution is not made before the account is assigned avaluePerSharefor the account debt distribution.This means that the account will experience whatever pending debt distribution there is from before the migration is made.
Recommendation
Update the account debt distribution for the new pool before assigning the
valuePerShareto the account with theassignDebtToAccountfunction. -
C-02 Critical unsaddle Reentrancy Resolved
Description
TreasuryMarket.unsaddle()repays the debt of the given user by withdrawing USD from the system, which increased thenetIssuanceD18of the market and in turn itstotalDebtas well. That's why_rebalance()is called afterunsaddlecompletes, to account for the change innetIssuanceD18by adjusting theartificialDebt.However, before calling
_rebalance(), the accountNFTis returned back to the user bysafeTransferFrom(). This will invoke the receiver'sonERC721Receivedand will allow them to call functions like saddle on the treasury market ormigrateDelegationto take advantage of the stale state.For example, a user may re-enter into the V3 core system by calling
migrateDelegationand stand to immediately benefit by the reportedDebt change that will occur directly after their migration via the_rebalancecall.Recommendation
Execute
_rebalance()before the ERC721 transfer. -
C-03 Critical saddle Is Not Enforced Acknowledged
Description
The Synthetix frontend will call
saddleon the TreasuryMarket contract whenever a user delegates to it's pool, but this function call is not enforced on-chain.When one or more users delegate collateral to the pool without calling
saddlethe first user who callssaddlewill receive a lot more debt than the user should, as thetargetDebtof the first user callingsaddleis calculated based on the total collateral in the vault:targetDebt = vaultCollateralValue.divDecimal(targetCratio).toInt();This either leads to the user not being able to
saddleas theassociateDebtcall reverts because this makes the user liquidatable, or theartificialDebtis inflated and the user acquires way more debt than the user should.Furthermore, this issue allows users to delegate a minimal amount and saddle it as debt before subsequently delegating a larger amount and allowing it to accumulate debt before saddling this amount as well and having this debt paid off by the treasury market without an additional loan.
Recommendation
Always call
saddlewhen a user delegates to the pool, in the same transaction. -
C-04 Critical artificialDebt Stays 0 If Debt == targetDebt Logical Error Resolved
Description
When a user enters the system (delegates to the pool and calls
saddle) with a CR equal to thetargetCratio, theartificialDebtwill stay 0 as the calculatedtargetDebtwill equal the account's debt, and theartificialDebtis increased like that:artificialDebt += targetDebt - accountDebt;The first user who calls
saddleafter that will receive a lot more debt than the user should, as thetargetDebtis calculated based on the total collateral in the vault, if theartificialDebtis 0 (because the system thinks this is the first user who enters the system):targetDebt = vaultCollateralValue.divDecimal(targetCratio).toInt();This either leads to the user not being able to
saddleas theassociateDebtcall reverts because this makes the user liquidatable, or theartificialDebtis inflated and the user acquires way more debt than the user should.Recommendation
Use a different method to check if this is the first user entering the system.
-
H-01 High Rebalance Sandwhich Attack Acknowledged
Description
The
rebalancefunction updates the Treasury market’s artificial debt to match the target collateralization ratio. This will often be done when sister markets attached to the same pool accrue debt such that the collateralization ratio is smaller than desired.However because the rebalance is an immediate stepwise change in the
reportedDebtof the treasury market, malicious actors can arbitrage the update and extract immediate value from a reduction in reported debt in the rebalance function.This arbitrage can take place by migrating to the new pool, triggering the rebalance function to reduce the reportedDebt of the treasury market and thus realizing a net debt reduction on your position, and then subsequently unsaddling with this reduced debt.
Recommendation
Consider making the rebalancing a smooth linear function that adjusts in real time in the
reportedDebtfunction rather than stepwise jumps which can be potentially arbitraged. -
H-02 High Missing Rebalance Updates Logical Error Acknowledged
Description
A rebalance should be enforced before calling
delegateandsaddle, as without it, the vault debt value would return a stale artificial debt.For example, if the price of the collateral token increases or decreases after the last rebalance and the artificial debt is not updated, the vault debt value considered in the following code line would be incorrect:
TreasuryMarket.sol#L175
It is important to note that the vault debt value depends on artificial debt via
reportedDebt.Failing to update this value would result in either inflated or deflated debt being assigned to accounts, impacting both
associateDebtandloanAmountcalculations.For
unsaddle, the impact is not persisted as it is forsaddledue to a rebalance occurring at the end. However, stalereportedDebtstill affects theaccountDebtbeing repaid.TreasuryMarket.sol#L231
This repayment happens by withdrawing USD.
Consider a case where the actual artificial debt is lower than what is stored in the contract state. In such a case, the
accountDebtrepaid would be higher than the original amount. Depending on available liquidity, this may unnecessarily cause a revert when it should not.Ultimately, this does not pose a long-term issue, as the increase in artificial debt due to rebalance in the end would compensate for the additional amount being repaid.
The
setTargetCRatiofunction is used to update thetargetCRatioof theTreasuryMarketand calls_rebalanceat the end.The
_rebalancefunction will reset theartificialDebtto zero if theartificialDebtis smaller or equal to thevaultDebtValueminus the calculatedtargetDebtThis is possible with a
targetCRatioupdate depending on the giventargetCRatioand the current state of the market and would lead to major consequences.The next user who calls
saddlewill receive a lot more debt than the user should, as thetargetDebtof the first user callingsaddleis calculated based on the total collateral in the vault:targetDebt = vaultCollateralValue.divDecimal(targetCratio).toInt();This either leads to the user not being able to
saddleas theassociateDebtcall reverts because this makes the user liquidatable, or theartificialDebtis inflated and the user acquires way more debt than the user should.Recommendation
Consider enforcing a rebalance before both delegation + saddle, unsaddle and setTargetCRatio operations.
Note: A rebalance cannot be performed immediately before saddle. Instead, it must be executed before the combination of delegation and saddle to ensure accurate debt calculations.
-
H-03 High Rewards Can Be Siphoned By Migrations Logical Error Resolved
Description
In the
migrateDelegationfunction there is noupdateRewardsToVaultsfunction call for the new pool. As a result the pending rewards are not streamed to exclusively the prior existing delegators but now instead the pending reward distribution is now credited to the migrated delegate as well.Recommendation
Call
updateRewardsToVaultsfor the new pool in addition to the old pool. -
H-04 High Liquidation Issues Logical Error Acknowledged
Description
When users get liquidated in the core system, their collateral and debt is reset to zero and are distributed across the other participants in the given vault. This behavior causes problems in the
TreasuryMarket.When users
saddleand are liquidated before theunsaddlecall, theirsaddledCollateralvalue will stay as it is. Theunsaddleaction can not go through after this becausedelegateCollateralwill revert since it sets the user collateral to 0 and it is already zero since the user has been liquidated. The next time the user saddles,newlySaddledValuewill be heavily miscalculated. Either the transaction will revert because of subtraction underflow or it will go through using a wrongnewlySaddledValue(if the new saddled value is greater than what's recorded insaddledCollateral).Because
saddledCollateralis not 0,newlySaddledDebtwill be 0, even though the user might have migrated a large amount of debt. This perturbs the vault ratio and theartificialDebtcalculation. Furthermore, becausesaddledCollateralis not 0, the user's debt won't be created as a loan as well, so they won't have to pay it.The liquidation will increase the collateral and debt balance of the other participants as well. If one of these users calls
saddlewhile theirsaddledCollateralis not zero, the ratio will be perturbed again and their debt won't be added to their loan.Furthermore liquidated users are still able to pay off their treasury market loan with the adjustLoan function and may unknowingly be burning this payment amount as their account has already been liquidated.
Individual and vault liquidations are a risk in the event that either too much value is extracted with the treasuryMint function or if connecting markets experience a significant enough debt increase over time, which cannot be covered by the yield of the treasury market.
Recommendation
Firstly, consider adding validation in the
mintTreasuryfunction such that it is not possible to purposefully or accidentally withdraw sUSD to the point where it places the backing delegates or vault at increased risk of liquidation. E.g. do not allow the artificialDebt to be lowered below a specific threshold, since if it is lowered too far then the Treasury market will have insufficient ability to offset future pool debt increases.Secondly, consider preventing users who have been liquidated from paying off their treasury market loan by validating that the collateral amount in their account is nonzero.
Finally, ideally liquidations can be handled appropriately by the Treasury market. In order to do so the delegation/migration and saddling logic can check two specific states to determine if the account was previously liquidated and account for it accordingly.
The collateral of the account in the v3 system before the current delegation/migration can be validated against 0, if the previous delegated collateral amount is 0 then the saddled collateral can be reset to 0 as the account was liquidated. Finally if the vault epoch has increased relative to the last recorded vault epoch in saddle for this account then the account saddled collateral can be reset.
-
H-05 High Parallel Market Liquidation Risk Acknowledged
Description
The Treasury market uses artificial debt to compensate for what the target debt should be according to the target collateral ratio, and reports this as
reportedDebtto the pool.This artificial debt is calculated by considering total collateral value and total debt of the vault, including parallel markets.
According to our communication with the SNX team, the pool for Treasury market is designed to have other parallel markets. However, these parallel markets neither consider artificial debt nor trigger rebalancing of the Treasury market when their debt positions change.
Consequently, when a new debt position is taken according to the allowed weight on any parallel market, that debt is double-counted in artificial debt, and therefore in reported debt, causing total vault debt to be inflated.
This inflation increases the debt-collateral ratio for the vault and raises liquidation risk until the Treasury market is rebalanced.
An adversary can exploit this flaw by causing parallel markets to report a high debt for a short period, and potentially triggering a vault liquidation or individual delegation liquidations before the protocol or other parties have a chance to rebalance.
Recommendation
Consider making the
reportedDebtfunction a smooth function which does not depend on a rebalancing to occur in the treasury market. -
M-01 Medium Pool collateral limit can be bypassed Resolved
Description
When users delegate collateral to a pool,
VaultModuleensures the new delegation is not over the maximum allowed value for that pool by callingcheckPoolCollateralLimit().This restriction can be bypassed with the new
migrateDelegation()functionality. Users can migrate their collateral from a pool with higher limit to another with a lower one and put the second pool over its allowed limit.Because the
checkPoolCollateralLimitwill revert if the token to be delegated is not supported by the pool, this makes it possible for users to delegate a collateral token from pool X to another pool Y which doesn't support that token.Recommendation
Add a
checkPoolCollateralLimit()call for the new pool inmigrateDelegation() -
M-02 Medium Smart Contract Accounts Cannot Unsaddle Resolved
Description
The
unsaddlefunction uses ERC721’ssafeTransferFromfunction to transfer an account token back to the sender address. However if the sender address is a Smart Contract without anonERC721Receivedfunction then this transfer reverts and the unsaddle cannot take place.Recommendation
Consider using the normal transferFrom from the ERC721 interface so that even contracts which do not have an
onERC721Receivedimplementation may unsaddle. The fact that this sending address owns an account NFT is evidence enough that the contract can sufficiently handle an account NFT. -
M-03 Medium Race Condition in Adjust Loan and Unsaddle Acknowledged
Description
The purpose of adjustLoan is to allow users to repay their initial debt with a penalty and then unsaddle to exit the market. However, since these are separate actions, another user could execute unsaddle in between, withdrawing the liquidity created by first user's adjustLoan, leaving first user waiting for a new saddle.
Recommendation
Consider making adjustLoan and unsaddle an atomic action by providing helper function.
-
M-04 Medium Missing Vault C-ratio Validation Partially resolved
Description
In the
migrateDelegationfunction there is no validation that the new vault c-ratio is not in a liquidatable state during the migration. If this is the case then the migration will result in a position that is immediately liquidated along with the vault.Furthermore, there is no validation that the old vault c-ratio is valid at the time of migration. This means users may migrate out of a liquidatable vault to avoid the vault liquidation.
Recommendation
Include the
_verifyPoolCratiovalidation in themigrateDelegationfunction for both the old vault and the new vault similarly to thedelegateCollateralfunction. -
M-05 Medium Early Return In Rebalance May DoS The Market Acknowledged
Description
When the treasury mints new snxUSD, artificial debt decreases because actual debt increases on the V3 system side. Consider a scenario where the treasury mints to full capacity, setting
artificial_debt = 0.If the treasury later repays some debt using
burnTreasury, this reduces actual debt on the V3 system. This should increaseartificial_debtto reflect the target debt. However, this doesn't happen because rebalancing returns early when artificial debt is zero.TreasuryMarket.sol L413 function _rebalance() internal { if (artificialDebt == 0) { return; } ...As a result, once artificial debt reaches zero, if no new saddle is done, since
burnTreasurycan’t restore the artificial debt. It blocks both unsaddle (since unsaddle requiresartificialDebt > 0due toartificialDebt -= repayment) and furthermintTreasurycalls.Recommendation
Consider removing the early return
artificialDebt == 0case in the_rebalancefunction. -
M-06 Medium Unsaddle May Be Blocked For Last User Logical Error Acknowledged
Description
During the unsaddle function, a check is performed which prevents the last user from exiting if they are the only remaining LP in the vault, as their collateral constitutes the entire vault balance.
Recommendation
The check could be modified to:
if (accountCollateral > vaultCollateral) { revert ParameterError.InvalidParameter( "accountCollateral", "no surplus collateral to fund exit" ); } -
M-07 Medium Exit DoS If accountDebt > artificialDebt Acknowledged
Description
When users call
saddletheartificialDebtis incremented by the difference between the calculatedtargetDebtand the account's debt.Between
saddleandunsaddlethe debt of users can increase when the debt of the legacy market rises. When users callunsaddletheartificialDebtis decremented by the debt of the accountTherefore depending on the user's CR at
saddletime and the accrued debt from the legacy market betweensaddleandunsaddlethe debt of all users which can unsaddle can be bigger than theartificialDebt.The
unsaddlefunction is used to bring the account`s debt to 0 and undelegate the account from the pool. To accomplish this the market will:- withdraw
snxUSD - deposit the funds into the user's account
- burn the
snxUSDin the user's account to decrease the account's debt - undelegate the account from the pool (only possible with zero debt)
But as the withdrawal of
snxUSDwill increase the total debt of the treasury market and therefore the user's debt it is necessary to either decrease the total debt of the market or burn even moresnxUSDto bring the user's debt to 0. The protocol decided to decrease the total debt of the market by decreasing theartificialDebtbefore callingwithdrawMarketUsd.But as mentioned above the account's debt can be bigger than the
artificialDebtand as thereportedDebtis a capped at 0 it is not possible to decrease the debt of the market below 0. Theunsaddlefunction is therefore not able to bring the user's debt to 0 if the user's debt is bigger than theartificialDebt. This will lead to a DoS and frozen user funds as the attempt to undelegate the account will fail if there is still debt left in the account.Recommendation
If the
artificialDebtdecrement hits zero, withdraw and deposit excess funds to bring the account's debt to 0. - withdraw
-
L-01 Low Unnecessary Sender Computation Resolved
Description
In the
unsaddlefunction theaccountTokenowner validation re-computes the sender address in theUnauthorizedrevert data when instead the same storedsenderaddress may be used.Recommendation
Consider using the same
senderstack variable in theUnauthorizedrevert data instead of re-computing the sender. -
L-02 Low Unnecessary Current Loan Calculation Resolved
Description
In the
unsaddlefunction thecurrentLoanvalue is computed twice. Once on line 222 to compute thecurrentLoanvalue and validate that it is 0. And a second time on line 224 in theOutstandingLoanrevert data to compute theoutstandingLoanAmountvalue.Recommendation
Instead of recomputing the
currentLoanvalue, re-use thecurrentLoanvariable in theOutstandingLoanrevert data. -
L-03 Low Delegates Cannot Perform Actions Acknowledged
Description
In the
unsaddleandadjustLoanfunctions there is validation to ensure that only the owner of an account token is allowed to unsaddle. However in the core V3 system there are delegation roles which allow third parties to manage the collateral and other features of token accounts.The lack of delegation access to the unsaddle function may limit access for third party systems to the Treasury market.
Recommendation
Consider allowing delegates for an account with a certain role to unsaddle or adjust the loan for the account owner.
-
L-04 Low Unsaddle Prevented By Min Delegation Time Acknowledged
Description
The act of unsaddling includes an undelegation from the backing pool, however this may unexpectedly revert for users when their unsaddling action is completed before the end of the minimum delegation period for an attached market in the backing pool.
Recommendation
This may be the expected behavior and necessary to ensure sufficient delegation time, however it should be documented for users and integrators of the treasury market.
-
L-05 Low Treasury Market Out Of Range Risk Acknowledged
Description
In some exceptional edge cases it may be possible for the Treasury market to be bumped out of range if the debt per credit capacity limit is hit. This will remove the treasury market’s ability to control the pool’s debt and can lead to a DoS for users attempting to unsaddle their positions.
Recommendation
Adding validations on the lowest the artificial debt is able to be assigned during the
mintTreasuryfunction and perhaps even theunsaddlefunction can sufficiently address the risk of this edge case ever arising. -
L-06 Low Wrong comment Acknowledged
Description
The following comment says
Cannot migrate if c-ratio is in liquidation, but the next line doesn't check the c-ratio, it updates the debt distribution chain.Recommendation
Update the comment.
-
L-07 Low Missing Event On Critical Param Change Acknowledged
Description
There are two functions that allow the owner to change critical parameters of the treasury market:
setTargetCratioandsetDebtDecayFunction. ThesetTargetCratiofunction emits an event, but thesetDebtDecayFunctiondoes not. Best practice is to emit an event when critical system parameters are updated.Recommendation
Consider adding an event for the
setDebtDecayFunctionfunction. -
L-08 Low Loan duration is not validated Acknowledged
Description
There are validations inside
setDebtDecayFunctionfor each parameter excepttime. This means thedurationof the loan can be set to an unreasonable value.Recommendation
Validate the
timeparameter as well. -
L-09 Low Incomplete comment Acknowledged
Description
The comment inside
TreasuryMarket.minimumCredit()sayswe lock collateral here becauseand doesn't explain why.Recommendation
Complete the comment by explaining that users should not be able to undelegate from the pool and also migrate from it.
-
L-10 Low Missing Interface Support Acknowledged
Description
The
supportsInterfacefunction does not return that theIERC721Receiverinterface is supported.Recommendation
Consider adding the
IERC721Receiverinterface to thesupportsInterfacefunction. -
L-11 Low Reported Debt Does Not Validate marketId Acknowledged
Description
In the function
reportedDebt, the argumentrequestedMarketIdis never validated. This differs from other markets, whererequestedMarketIdis checked against the current market and returns0if there is no match.Without this validation, integration errors could occur, potentially leading to unintended behavior.
Recommendation
Validate
requestedMarketIdto matchmarketId. -
L-12 Low New Stakers Can Join Pool Logical Error Acknowledged
Description
According to SIP-420, only existing SNX stakers should be allowed to join the new protocol-owned debt pool.
However, in the current implementation, new stakers can bypass this restriction by using
delegateCollateral. This could contradict the intended design of the Treasury Market and should be addressed.Recommendation
To strictly abide by SIP-420, consider enabling only
migrateDelegation. -
L-13 Low Treasury Market Lacks Upside Acknowledged
Description
In the
unsaddlefunction the Treasury market only pays the user’s outstanding debt if it is positive, but does not take the user’s accumulated profit when the accumulated debt is negative.This means that the Treasury market does not hold exposure to the profits in excess of the original debt assigned to the account. This should not be the case because it presents an unfairly beneficial offer to the user at the detriment of the protocol.
Users can rest assured that any additional debt accumulated over the saddled period will be covered by the treasury market, but still have exposure to the upside from profits generated by markets attached to the pool.
This is an increasingly large issue because users are not required to unsaddle as soon as their loan has expired, they can remain saddled for an extended period of time to reap the benefits of having their downsides covered while still holding exposure to the upside.
Recommendation
Mint any negative debt value to the treasury address to take the upside of the user’s position for the treasury market.
-
L-14 Low Vault Liquidation via Unsaddle and Max Withdrawable Logical Error Acknowledged
Description
In unsaddle, the treasury market enables users to exit their positions by subtracting neededToPay from artificialDebt and withdrawing USD from the pool:
artificialDebt -= int256(neededToRepay); v3System.withdrawMarketUsd(marketId, address(this), neededToRepay);One would assume that
artificialDebt > neededToRepayis always enforced, aswithdrawMarketUsdshould revert otherwise. However, as demonstrated in the Proof of Concept,getWithdrawableMarketUsdallows users to withdraw funds up to the collateral value instead.Exploit Scenario
If
neededToRepay > artificialDebt, the user can withdraw more than **artificialDebt **to close their saddle position, exposing the entire vault to liquidation risk.Example (From POC):
- User A has 4 ETH of collateral delegated and 2 ETH of debt.
- They perform saddle.
- Since the debt capacity is fully utilized,
artificialDebtbecomes zero, meaning the vault is already at maximum utilization. - Ideally, further withdrawals should be blocked.
- However, the PoC demonstrates that an additional 2 ETH can still be withdrawn. (PoC uses
mintTreasuryto demonstratewithdrawMarketUsdfor simplicity, but the same effect could be achieved byunsaddleas well.rebalancewon't revert either, since it would resetartificialDebtto 0) - This results in a total vault debt of 4 ETH, making it vulnerable to liquidation.
POC:
function test_liq_vault() external { sideMarket.setReportedDebt(2 ether); sideMarket.callAssociateDebt(poolId, address(collateralToken), accountId, 2 ether); market.saddle(accountId); console.log('artificialDebt', market.reportedDebt(0)); console.log('withdrawable: ', v3System.getWithdrawableMarketUsd(market.marketId())); vm.prank(market.owner()); market.mintTreasury(2 ether); console.log(v3System.isVaultLiquidatable(poolId, address(collateralToken))); }Recommendation
Consider enforcing
artificialDebt >= neededToPayinunsaddle. Furthermore, it may be prudent to add a similar validation in themintTreasuryfunction to avoid treasury minting from putting the vault or individual positions at liquidation risk. -
L-15 Low Unsaddle Race Condition Logical Error Acknowledged
Description
The treasury market tries to earn yield on Ethena and repay the debt of all users with it which delegated to the market for a given amount of time.
If for any reason the market is not able to earn enough funds to do that the users who
unsaddlefirst will get all of their debt forgiven and the others will be stuck in the system.Recommendation
Consider adding a fair mechanism so that all users can exit under the same conditions in such an emergency situation. For example everyone gets only a portion of their debt forgiven and they are able to pay the rest themselves. Instead of a few users getting all of their debt forgiven and the others being stuck.
Remediation Review
14 findings · February 11, 2025-
C-01 Critical Low Cratio Accounts Immediately Gain Acknowledged
Description
Because delegation/migration and saddling is not atomically enforced to occur in the same transaction a malicious actor can go around the UI enforcements and steal value from the rebalancing system.
The malicious actor can specifically put their account in a C-ratio below the target C-ratio of the treasury market before calling the
migrateDelegationfunction. Then upon having their position migrated to the new pool, the actor can trigger a rebalance on the Treasury market which will correct for their lower C-ratio and immediately forgive a portion of their debt.This attack can be repeated with large volume many times to drain the pool of nearly all USD value and ultimately liquidate the backing vault.
Recommendation
Require that the delegateCollateral and migrateCollateral functions call into the Treasury market saddle function to automatically saddle the user in the same transaction at the contract level.
-
H-01 High burnUsd Perturbs Locks Logical Error Acknowledged
Description
In the
burnUsdfunction there is no validation that the amount of sUSD collateral being burned to repay debt is not locked.This allows the account to enter an invalid state of having locked collateral larger than their current collateral balance. And furthermore allows the account to bypass the lock mechanism and extract value from their locked collateral by immediately paying down debt with it.
A user can trivially deposit sUSD collateral, lock it, and whenever they wish to withdraw it before the lock they can mintUsd to accrue debt and burn the locked sUSD to repay the debt.
Recommendation
Validate that the burned collateral is not locked in the
burnUsdfunction. -
H-02 High Incentive For Bypassing UI-Enforced Rebalance Acknowledged
Description
To address the previously reported H2 issue, the Synthetix team proposed triggering rebalance from the UI for each interaction instead of enforcing it via the contract.
However, this approach introduces following problem:
If the collateral value drops and an account’s debt exceeds the actual target debt, the following check should ideally prevent saddling:
if (accountDebt > targetDebt) { revert InsufficientCRatio(accountId, accountDebt.toUint(), targetDebt.toUint()); }However, if rebalance is not executed before calling delegate and saddle, the
targetDebtremains inflated, allowing users to saddle even when their actual C-ratio is too low.Ultimately, the treasury is forced to cover the shortfall, creating an unintended liability. These accounts benefit from partial debt repayment in the next rebalance, effectively shifting the burden to the treasury.
Since rebalance is not enforced on-chain, users can manually call delegate and saddle, bypassing the UI-triggered rebalance.
Recommendation
To prevent this exploit, rebalance should be enforced at the contract level for any combination of
delegate/migrateandsaddle. -
M-01 Medium Early Return In Rebalance May DoS The Market Logical Error Acknowledged
Description
When the treasury mints new snxUSD, artificial debt decreases because actual debt increases on the V3 system side. Consider a scenario where the treasury mints to full capacity, setting
artificial_debt = 0.If the treasury later repays some debt using
burnTreasury, this reduces actual debt on the V3 system. This should increaseartificial_debtto reflect the target debt. However, this doesn't happen because rebalancing returns early when artificial debt is zero.TreasuryMarket.sol L413 function _rebalance() internal { if (artificialDebt == 0) { return; } ...As a result, once artificial debt reaches zero, if no new saddle is done, since
burnTreasurycan’t restore the artificial debt. It blocks both unsaddle (since unsaddle requiresartificialDebt > 0due toartificialDebt -= repayment) and furthermintTreasurycalls.Recommendation
Consider removing the early return
artificialDebt == 0case in the_rebalancefunction. -
M-02 Medium Users Can Saddle Below 200% C-Ratio, Shifting Debt to Treasury Acknowledged
Description
In
saddle(), users are allowed to saddle as long as their account debt remains ≤ target debt.function saddle(uint128 accountId) external override { /........... targetDebt = vaultCollateralValue.divDecimal(targetCratio).toInt(); } else if (vaultCollateralValue > accountCollateralValue) { ......... targetDebt = (accountCollateralValue.toInt() * (vaultDebtValue - newlySaddledDebt)) / (vaultCollateralValue - newlySaddledValue).toInt(); } if (accountDebt > targetDebt) { revert InsufficientCRatio(accountId, accountDebt.toUint(), targetDebt.toUint()); } ........ }The target debt is intended to maintain a target collateralization ratio (C-ratio) of 200% relative to the vault’s collateral. Currently, the target debt calculation is done proportionally to the vault's existing debt.
This approach works **as long as the calculated **
targetDebt <= vaultCollateralValue.divDecimal(targetCratio).toInt()Once
targetDebt > vaultCollateralValue.divDecimal(targetCratio).toInt()becomes issue as demonstrated in following case,—users would be able to saddle even if their individual account debt exceeds the 200% collateralization ratio. Once a user saddles under such conditions, the burden of repaying the excess debt shifts to the treasury.Example Scenario:
- Initial State
- Collateral Value: 4 ETH
- Debt: 2 ETH
- Artificial Debt: 0 ETH
- C-ratio: 200% (4 ETH / 2 ETH)
- Treasury Minted Debt: 2 ETH
- Collateral Price Drops by 10%
- Updated Collateral Value: 3.6 ETH
- Debt: 2 ETH
- Artificial Debt: 0 ETH
- New C-ratio: 180% (3.6 ETH / 2 ETH)
- Issue with Saddle Mechanism
- At this point, the user should only be able to saddle if their C-ratio ≥ 200%
- However, since the system recalculates the target debt based on the adjusted vault debt, it still allows saddling even when the user's individual C-ratio falls below 200%.
- The user, now having 3.6 ETH collateral value with 2 ETH debt, can still saddle, leading to a treasury liability.
Recommendation
Even if you:
- Rebalance before delegating + saddle,
- Use atomic delegation + saddle or migrate delegation + saddle,
the issue still persists.
Consider comparing account debt to
min (targetDebt, accountCollateralValue.divDecimal(targetCratio).toInt())It is also advised to the Treasury that they should never mint the total available capacity and should keep some buffer for artificial debt to absorb price updates like this.
- Initial State
-
M-03 Medium Potential Systemic Risks of Disabling Liquidations Logical Error Acknowledged
Description
To address the previously reported H-04/H-05 issues, the Synthetix team has proposed completely disabling liquidations for the associated pool and allowing unsaddling on a first-come, first-served (FCFS) basis.
However, we would like to highlight the potential impact of this decision—not just on the Treasury Market, but on the entire SNX ecosystem if such a scenario unfolds.
In V3 Core, the sUSD used within Treasury Market is fundamentally no different from the sUSD used in other pools.
Therefore, if Treasury Market’s pool becomes insolvent—meaning the sUSD minted by the Treasury is no longer fully backed—it could have system-wide consequences for SNX.
If the reasoning behind this decision is that the Treasury controls the minting and burning of sUSD for the associated pool. and would therefore hold any unbacked balance, this assumption is not entirely accurate.
Whenever the Treasury swaps sUSD for Ethena USD to earn additional yield, the sUSD minted within Treasury Market enters the open market.
If insolvency occurs, a significant amount of unbacked sUSD could lead to a bank run, where users rush to withdraw collateral from other pools, or swapping it to other stables, potentially causing an sUSD depeg.
Recommendation
Reconsider the decision to completely disable liquidations for the Treasury Market’s associated pool, as it introduces systemic risk to the SNX ecosystem.
-
M-04 Medium The Migrated From Vault Could Become Unhealthy Acknowledged
Description
The
_verifyPoolCratiovalidation is not performed on the old pool in themigrateDelegationfunction.As a result a user may make the from vault liquidatable by removing their delegation from that vault.
Recommendation
Perform the
_verifyPoolCratiofor the old pool in themigrateDelegationfunction. -
M-06 Medium Missing Check In mintTreasury Acknowledged
Description
The protocol only partially fixed the
L-14 Vault Liquidation via Unsaddle and Max Withdrawablefinding and left themintTreasuryfunction unchanged. This report describes how the likelihood of the issue in themintTreasuryfunction can be increased.The owner of the protocol could accidentally mint the vault into a liquidatable state in the following scenario:
- The protocol owner creates a transaction to mint a lot of sUSD (but not so much that the vault will be liquidated)
- Malicious actor front runs and unsaddles a large amount
- The transaction of the protocol goes through and the vault is left in a liquidatable state
Recommendation
Ensure in the
mintTreasuryfunction that the minted amount will not push the vault into a liquidatable state. -
M-07 Medium Unsaddle May Be Blocked For Last User Logical Error Acknowledged
Description
During the unsaddle function, a check is performed which prevents the last user from exiting if they are the only remaining LP in the vault, as their collateral constitutes the entire vault balance.
Recommendation
The check could be modified to:
if (accountCollateral > vaultCollateral) { revert ParameterError.InvalidParameter( "accountCollateral", "no surplus collateral to fund exit" ); } -
L-01 Low Missing requireSufficientDelegation Validation Logical Error Acknowledged
Description
As of now,
migrateDelegationdoes not enforce a minimum delegation amount, unlikedelegateCollateral.If the intention is to keep
minDelegationAmountas 0, as specified in the test configuration, then this is not an issue. However, if there are plans to introduce such a limit in the future, consider addressing this.Recommendation
Consider adding check of
requireSufficientDelegationinside migrateDelegation. -
L-02 Low Invalid Loan Completion Percentage Logical Error Acknowledged
Description
In the
_repaymentPenaltyfunction the loan completion percentage is assigned to 0 in the event that the loan duration is 0.However in the event that the loan duration is 0 then the loan would be instantly entirely completed.
Recommendation
Assign the
loanCompletionPercentageto 1e18 in the event that the loan duration is 0. -
L-03 Low Insufficient Check In _repaymentPenalty Acknowledged
Description
The
_repaymentPenaltyfunction early returns zero if the target loan is greater than the current loan as no changes are made in that case:if (targetLoan > currentLoan || currentPenalty == 0) { return 0; }A
>=makes more sense here as nothing changes in this case either.Recommendation
Change the mentioned line to
if (targetLoan > currentLoan || currentPenalty == 0) { return 0; }. -
L-04 Low setDebtDecayFunction May Be Called Too Late Logical Error Acknowledged
Description
The market starts when the
registerMarketfunction is called which already sets a defaulttargetCratiobut all the debt decaying params are not set to default values here.Therefore if the market is deployed and registered and a user enters into the market before the debt decaying params are set, the user gets a 0 loan power and 0 loan duration.
These are very bad conditions for the user the loan will not decay and the user has to pay the maximum penalty when he wants to exit.
Recommendation
Consider setting default debt decaying params in the
registerMarketfunction. To ensure that everyone enters the market with fair conditions. -
L-05 Low Treasury Debt Management Warning Acknowledged
Description
The treasury has a responsibility to carefully manage the artificial debt of the treasury market.
It is important that the treasury does not mint the entire available amount from the treasury market so that a reasonable amount of artificial debt is left behind to absorb price movements and side market activities.
Recommendation
Be aware that the treasury market must carefully balance the artificial debt in this way.
No findings match.
More from Synthetix
All 14 reports-
Update Reviews
34 findings2 critical · 4 high 34 findings: 2 critical, 4 high, 13 medium, 10 low, 5 informational -
Deposit Contract
38 findings1 high 38 findings: 1 high, 6 medium, 20 low, 11 informational -
Fixed Staking Rewards
6 findings1 high 6 findings: 1 high, 2 medium, 3 low -
Auto-Compounding LP Vault
80 findings1 critical · 4 high 80 findings: 1 critical, 4 high, 14 medium, 61 low
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
