Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · February 2025

Treasury Market

for Synthetix

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

7 resolved · 1 partially resolved · 37 acknowledged

Scope

3 files in scope · 297 nSLOC
FilenSLOCLines
markets/treasury-market/contracts/TreasuryMarket.sol285434
markets/treasury-market/contracts/SynthetixTreasuryProxy.sol817
markets/treasury-market/contracts/InitialModuleBundle.sol412

Findings 45

Main Review

31 findings · January 29, 2025
  1. C-01 Critical New Pool Debt Distribution Not Updated Logical Error Acknowledged
    Location
    VaultModule.sol
    Round
    Main Review

    Description

    In the migrateDelegation function there is no updateAccountDebt invocation for the new pool, therefore the pending debt distribution is not made before the account is assigned a valuePerShare for 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 valuePerShare to the account with the assignDebtToAccount function.

  2. C-02 Critical unsaddle Reentrancy Resolved
    Location
    TreasuryMarket.sol
    Round
    Main Review

    Description

    TreasuryMarket.unsaddle() repays the debt of the given user by withdrawing USD from the system, which increased the netIssuanceD18 of the market and in turn its totalDebt as well. That's why _rebalance() is called after unsaddle completes, to account for the change in netIssuanceD18 by adjusting the artificialDebt.

    However, before calling _rebalance(), the account NFT is returned back to the user by safeTransferFrom(). This will invoke the receiver's onERC721Received and will allow them to call functions like saddle on the treasury market or migrateDelegation to take advantage of the stale state.

    For example, a user may re-enter into the V3 core system by calling migrateDelegation and stand to immediately benefit by the reportedDebt change that will occur directly after their migration via the _rebalance call.

    Recommendation

    Execute _rebalance() before the ERC721 transfer.

  3. C-03 Critical saddle Is Not Enforced Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    The Synthetix frontend will call saddle on 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 saddle the first user who calls saddle will receive a lot more debt than the user should, as the targetDebt of the first user calling saddle is calculated based on the total collateral in the vault:

    targetDebt = vaultCollateralValue.divDecimal(targetCratio).toInt();

    This either leads to the user not being able to saddle as the associateDebt call reverts because this makes the user liquidatable, or the artificialDebt is 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 saddle when a user delegates to the pool, in the same transaction.

  4. C-04 Critical artificialDebt Stays 0 If Debt == targetDebt Logical Error Resolved
    Location
    TreasuryMarket.sol: 186
    Round
    Main Review

    Description

    When a user enters the system (delegates to the pool and calls saddle) with a CR equal to the targetCratio, the artificialDebt will stay 0 as the calculated targetDebt will equal the account's debt, and the artificialDebt is increased like that:

    artificialDebt += targetDebt - accountDebt;

    The first user who calls saddle after that will receive a lot more debt than the user should, as the targetDebt is calculated based on the total collateral in the vault, if the artificialDebt is 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 saddle as the associateDebt call reverts because this makes the user liquidatable, or the artificialDebt is 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.

  5. H-01 High Rebalance Sandwhich Attack Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    Description

    The rebalance function 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 reportedDebt of 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 reportedDebt function rather than stepwise jumps which can be potentially arbitraged.

  6. H-02 High Missing Rebalance Updates Logical Error Acknowledged
    Location
    https://github.com/GuardianAudits/treasury-market-2/blob/c6a45fc10ca9168524d8bc655b31521a8df5c9d5/markets/treasury-market/contracts/TreasuryMarket.sol#L175, https://github.com/GuardianAudits/treasury-market-2/blob/c6a45fc10ca9168524d8bc655b31521a8df5c9d5/markets/treasury-market/contracts/TreasuryMarket.sol#L231
    Round
    Main Review

    Description

    A rebalance should be enforced before calling delegate and saddle, 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 associateDebt and loanAmount calculations.

    For unsaddle, the impact is not persisted as it is for saddle due to a rebalance occurring at the end. However, stale reportedDebt still affects the accountDebt being 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 accountDebt repaid 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 setTargetCRatio function is used to update the targetCRatio of the TreasuryMarket and calls _rebalance at the end.

    The _rebalance function will reset the artificialDebt to zero if the artificialDebt is smaller or equal to the vaultDebtValue minus the calculated targetDebt

    This is possible with a targetCRatio update depending on the given targetCRatio and the current state of the market and would lead to major consequences.

    The next user who calls saddle will receive a lot more debt than the user should, as the targetDebt of the first user calling saddle is calculated based on the total collateral in the vault:

    targetDebt = vaultCollateralValue.divDecimal(targetCratio).toInt();

    This either leads to the user not being able to saddle as the associateDebt call reverts because this makes the user liquidatable, or the artificialDebt is 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.

  7. H-03 High Rewards Can Be Siphoned By Migrations Logical Error Resolved
    Location
    VaultModule.sol: 160
    Round
    Main Review

    Description

    In the migrateDelegation function there is no updateRewardsToVaults function 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 updateRewardsToVaults for the new pool in addition to the old pool.

  8. H-04 High Liquidation Issues Logical Error Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    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 saddle and are liquidated before the unsaddle call, their saddledCollateral value will stay as it is. The unsaddle action can not go through after this because delegateCollateral will 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, newlySaddledValue will be heavily miscalculated. Either the transaction will revert because of subtraction underflow or it will go through using a wrong newlySaddledValue (if the new saddled value is greater than what's recorded in saddledCollateral).

    Because saddledCollateral is not 0, newlySaddledDebt will be 0, even though the user might have migrated a large amount of debt. This perturbs the vault ratio and the artificialDebt calculation. Furthermore, because saddledCollateral is 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 saddle while their saddledCollateral is 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 mintTreasury function 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.

  9. H-05 High Parallel Market Liquidation Risk Acknowledged
    Location
    TreasuryMarket.sol L104, L365
    Round
    Main Review

    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 reportedDebt to 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 reportedDebt function a smooth function which does not depend on a rebalancing to occur in the treasury market.

  10. M-01 Medium Pool collateral limit can be bypassed Resolved
    Location
    VaultModule.sol
    Round
    Main Review

    Description

    When users delegate collateral to a pool, VaultModule ensures the new delegation is not over the maximum allowed value for that pool by calling checkPoolCollateralLimit().

    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 checkPoolCollateralLimit will 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 in migrateDelegation()

  11. M-02 Medium Smart Contract Accounts Cannot Unsaddle Resolved
    Location
    TreasuryMarket.sol: 265
    Round
    Main Review

    Description

    The unsaddle function uses ERC721’s safeTransferFrom function to transfer an account token back to the sender address. However if the sender address is a Smart Contract without an onERC721Received function 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 onERC721Received implementation may unsaddle. The fact that this sending address owns an account NFT is evidence enough that the contract can sufficiently handle an account NFT.

  12. M-03 Medium Race Condition in Adjust Loan and Unsaddle Acknowledged
    Location
    TreasuryMarket.sol L296 L211
    Round
    Main Review

    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.

  13. M-04 Medium Missing Vault C-ratio Validation Partially resolved
    Location
    VaultModule.sol: 160
    Round
    Main Review

    Description

    In the migrateDelegation function 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 _verifyPoolCratio validation in the migrateDelegation function for both the old vault and the new vault similarly to the delegateCollateral function.

  14. M-05 Medium Early Return In Rebalance May DoS The Market Acknowledged
    Location
    TreasuryMarket.sol L413
    Round
    Main Review

    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 increase artificial_debt to 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 burnTreasury can’t restore the artificial debt. It blocks both unsaddle (since unsaddle requires artificialDebt > 0 due to artificialDebt -= repayment) and further mintTreasury calls.

    Recommendation

    Consider removing the early return artificialDebt == 0 case in the _rebalance function.

  15. M-06 Medium Unsaddle May Be Blocked For Last User Logical Error Acknowledged
    Location
    TreasuryMarket.sol: 240
    Round
    Main Review

    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"
    	);
    }
    
  16. M-07 Medium Exit DoS If accountDebt > artificialDebt Acknowledged
    Location
    TreasuryMarket.sol: 247-249
    Round
    Main Review

    Description

    When users call saddle the artificialDebt is incremented by the difference between the calculated targetDebt and the account's debt.

    Between saddle and unsaddle the debt of users can increase when the debt of the legacy market rises. When users call unsaddle the artificialDebt is decremented by the debt of the account

    Therefore depending on the user's CR at saddle time and the accrued debt from the legacy market between saddle and unsaddle the debt of all users which can unsaddle can be bigger than the artificialDebt.

    The unsaddle function 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 snxUSD in 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 snxUSD will 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 more snxUSD to bring the user's debt to 0. The protocol decided to decrease the total debt of the market by decreasing the artificialDebt before calling withdrawMarketUsd.

    But as mentioned above the account's debt can be bigger than the artificialDebt and as the reportedDebt is a capped at 0 it is not possible to decrease the debt of the market below 0. The unsaddle function is therefore not able to bring the user's debt to 0 if the user's debt is bigger than the artificialDebt. 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 artificialDebt decrement hits zero, withdraw and deposit excess funds to bring the account's debt to 0.

  17. L-01 Low Unnecessary Sender Computation Resolved
    Location
    TreasuryMarket.sol: 219
    Round
    Main Review

    Description

    In the unsaddle function the accountToken owner validation re-computes the sender address in the Unauthorized revert data when instead the same stored sender address may be used.

    Recommendation

    Consider using the same sender stack variable in the Unauthorized revert data instead of re-computing the sender.

  18. L-02 Low Unnecessary Current Loan Calculation Resolved
    Location
    TreasuryMarket.sol: 224
    Round
    Main Review

    Description

    In the unsaddle function the currentLoan value is computed twice. Once on line 222 to compute the currentLoan value and validate that it is 0. And a second time on line 224 in the OutstandingLoan revert data to compute the outstandingLoanAmount value.

    Recommendation

    Instead of recomputing the currentLoan value, re-use the currentLoan variable in the OutstandingLoan revert data.

  19. L-03 Low Delegates Cannot Perform Actions Acknowledged
    Location
    TreasuryMarket.sol: 211, 298
    Round
    Main Review

    Description

    In the unsaddle and adjustLoan functions 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.

  20. L-04 Low Unsaddle Prevented By Min Delegation Time Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    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.

  21. L-05 Low Treasury Market Out Of Range Risk Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    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 mintTreasury function and perhaps even the unsaddle function can sufficiently address the risk of this edge case ever arising.

  22. L-06 Low Wrong comment Acknowledged
    Location
    VaultModule.sol:189
    Round
    Main Review

    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.

  23. L-07 Low Missing Event On Critical Param Change Acknowledged
    Location
    TreasuryMarket.sol: 344-363
    Round
    Main Review

    Description

    There are two functions that allow the owner to change critical parameters of the treasury market: setTargetCratio and setDebtDecayFunction. The setTargetCratio function emits an event, but the setDebtDecayFunction does not. Best practice is to emit an event when critical system parameters are updated.

    Recommendation

    Consider adding an event for the setDebtDecayFunction function.

  24. L-08 Low Loan duration is not validated Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    Description

    There are validations inside setDebtDecayFunction for each parameter except time. This means the duration of the loan can be set to an unreasonable value.

    Recommendation

    Validate the time parameter as well.

  25. L-09 Low Incomplete comment Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    Description

    The comment inside TreasuryMarket.minimumCredit() says we lock collateral here because and 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.

  26. L-10 Low Missing Interface Support Acknowledged
    Location
    TreasuryMarket.sol 381-390
    Round
    Main Review

    Description

    The supportsInterface function does not return that the IERC721Receiver interface is supported.

    Recommendation

    Consider adding the IERC721Receiver interface to the supportsInterface function.

  27. L-11 Low Reported Debt Does Not Validate marketId Acknowledged
    Location
    TreasuryMarket.sol: 104
    Round
    Main Review

    Description

    In the function reportedDebt, the argument requestedMarketId is never validated. This differs from other markets, where requestedMarketId is checked against the current market and returns 0 if there is no match.

    Without this validation, integration errors could occur, potentially leading to unintended behavior.

    Recommendation

    Validate requestedMarketId to match marketId.

  28. L-12 Low New Stakers Can Join Pool Logical Error Acknowledged
    Location
    Global
    Round
    Main Review

    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.

  29. L-13 Low Treasury Market Lacks Upside Acknowledged
    Location
    TreasuryMarket.sol: 211
    Round
    Main Review

    Description

    In the unsaddle function 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.

  30. L-14 Low Vault Liquidation via Unsaddle and Max Withdrawable Logical Error Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Main Review

    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 > neededToRepay is always enforced, as withdrawMarketUsd should revert otherwise. However, as demonstrated in the Proof of Concept, getWithdrawableMarketUsd allows 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):

    1. User A has 4 ETH of collateral delegated and 2 ETH of debt.
    2. They perform saddle.
    3. Since the debt capacity is fully utilized, artificialDebt becomes zero, meaning the vault is already at maximum utilization.
    4. Ideally, further withdrawals should be blocked.
    5. However, the PoC demonstrates that an additional 2 ETH can still be withdrawn. (PoC uses mintTreasury to demonstrate withdrawMarketUsd for simplicity, but the same effect could be achieved by unsaddle as well. rebalance won't revert either, since it would reset artificialDebt to 0)
    6. 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 >= neededToPay in unsaddle. Furthermore, it may be prudent to add a similar validation in the mintTreasury function to avoid treasury minting from putting the vault or individual positions at liquidation risk.

  31. L-15 Low Unsaddle Race Condition Logical Error Acknowledged
    Location
    Global
    Round
    Main Review

    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 unsaddle first 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
  1. C-01 Critical Low Cratio Accounts Immediately Gain Acknowledged
    Location
    Global
    Round
    Remediation Review

    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 migrateDelegation function. 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.

  2. H-01 High burnUsd Perturbs Locks Logical Error Acknowledged
    Location
    IssueUSDModule.sol: 115
    Round
    Remediation Review

    Description

    In the burnUsd function 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 burnUsd function.

  3. H-02 High Incentive For Bypassing UI-Enforced Rebalance Acknowledged
    Location
    TreasuryMarket.sol L201
    Round
    Remediation Review

    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 targetDebt remains 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/migrate and saddle.

  4. M-01 Medium Early Return In Rebalance May DoS The Market Logical Error Acknowledged
    Location
    TreasuryMarket.sol L413
    Round
    Remediation Review

    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 increase artificial_debt to 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 burnTreasury can’t restore the artificial debt. It blocks both unsaddle (since unsaddle requires artificialDebt > 0 due to artificialDebt -= repayment) and further mintTreasury calls.

    Recommendation

    Consider removing the early return artificialDebt == 0 case in the _rebalance function.

  5. M-02 Medium Users Can Saddle Below 200% C-Ratio, Shifting Debt to Treasury Acknowledged
    Location
    TreasuryMarket.sol L201
    Round
    Remediation Review

    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:
    1. 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
    2. 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)
    3. 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.

  6. M-03 Medium Potential Systemic Risks of Disabling Liquidations Logical Error Acknowledged
    Location
    Global
    Round
    Remediation Review

    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.

  7. M-04 Medium The Migrated From Vault Could Become Unhealthy Acknowledged
    Location
    VaultModule.sol: 245
    Round
    Remediation Review

    Description

    The _verifyPoolCratio validation is not performed on the old pool in the migrateDelegation function.

    As a result a user may make the from vault liquidatable by removing their delegation from that vault.

    Recommendation

    Perform the _verifyPoolCratio for the old pool in the migrateDelegation function.

  8. M-06 Medium Missing Check In mintTreasury Acknowledged
    Location
    TreasuryMarket.sol: 363-367
    Round
    Remediation Review

    Description

    The protocol only partially fixed the L-14 Vault Liquidation via Unsaddle and Max Withdrawable finding and left the mintTreasury function unchanged. This report describes how the likelihood of the issue in the mintTreasury function 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 mintTreasury function that the minted amount will not push the vault into a liquidatable state.

  9. M-07 Medium Unsaddle May Be Blocked For Last User Logical Error Acknowledged
    Location
    TreasuryMarket.sol: 240
    Round
    Remediation Review

    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"
    	);
    }
    
  10. L-01 Low Missing requireSufficientDelegation Validation Logical Error Acknowledged
    Location
    Missing `requireSufficientDelegation` in the migration function
    Round
    Remediation Review

    Description

    As of now, migrateDelegation does not enforce a minimum delegation amount, unlike delegateCollateral.

    If the intention is to keep minDelegationAmount as 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 requireSufficientDelegation inside migrateDelegation.

  11. L-02 Low Invalid Loan Completion Percentage Logical Error Acknowledged
    Location
    TreasuryMarket.sol: 467
    Round
    Remediation Review

    Description

    In the _repaymentPenalty function 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 loanCompletionPercentage to 1e18 in the event that the loan duration is 0.

  12. L-03 Low Insufficient Check In _repaymentPenalty Acknowledged
    Location
    TreasuryMarket.sol: 463-465
    Round
    Remediation Review

    Description

    The _repaymentPenalty function 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; }.

  13. L-04 Low setDebtDecayFunction May Be Called Too Late Logical Error Acknowledged
    Location
    TreasuryMarket.sol
    Round
    Remediation Review

    Description

    The market starts when the registerMarket function is called which already sets a default targetCratio but 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 registerMarket function. To ensure that everyone enters the market with fair conditions.

  14. L-05 Low Treasury Debt Management Warning Acknowledged
    Location
    Global
    Round
    Remediation Review

    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.

More from Synthetix

All 14 reports
  1. Update Reviews

    34 findings2 critical · 4 high 34 findings: 2 critical, 4 high, 13 medium, 10 low, 5 informational
  2. Deposit Contract

    38 findings1 high 38 findings: 1 high, 6 medium, 20 low, 11 informational
  3. Fixed Staking Rewards

    6 findings1 high 6 findings: 1 high, 2 medium, 3 low
  4. 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.

Get a quote