Guardian's review of Deposit Contract for Synthetix, published October 2025. The report records 38 findings across 3 review rounds, including 1 high and 6 medium.
- Published
- Review window
- August 28 to October 12, 2025
- Rounds
- Main Review, Remediation Review, Remediation Review 2
- Language
- Solidity
- Chains
- Ethereum, Optimism, Base, Arbitrum
- Sector
- Perpetuals
- 0 Critical
- 1 High
- 6 Medium
- 20 Low
- 11 Informational
Scope
4 files in scope · 763 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/SynthetixDepositContract.sol | 642 | 1007 |
src/libraries/ReasonCodes.sol | 17 | 31 |
src/interfaces/IPermit2.sol | 18 | 35 |
src/interfaces/ISynthetixDepositContract.sol | 86 | 293 |
Findings 38
Main Review
27 findings · August 28 to September 1, 2025-
H-01 High Profitable Traders Unfunded Logical Error Acknowledged
Description
The Deposit contract never posts on-chain PnL between counterparties. A winning trader's withdrawal can exceed their on-chain user balance and is still disbursed as long as the contract holds enough tokens, while no corresponding debit is applied to the losing trader. This is not zero-sum and allows profits to be realized without charging the other side of the trade, eventually leading to contract insolvency without appropriate risk engine/off-chain withdrawal validations.
Furthermore, the losing trader's user balance is closer to the
config.userMaximum, so if they would like to deposit more collateral to save their position from being liquidated, they may be prevented from doing so, although their true, off-chain balance is less than the user balance the on-chain mapping contains due to negative PnL.Recommendation
Have the relayer post an order settlement to the contract, such that the user balance of both the maker and taker are adjusted by the PnL.
-
M-01 Medium Token Pause Not Enforced On Disbursal Validation Resolved
Description
Mapping
tokenWithdrawalsPausedis intended to prevent withdrawals for specific tokens. FunctionrequestWithdrawalcheckstokenWithdrawalsPaused[token]but functiondisburseWithdrawaldoes not. Consequently assets that are intended to be paused may still leave the Deposit contract.Recommendation
Consider validation again
tokenWithdrawalsPausedin functiondisburseWithdrawal. -
M-02 Medium Expired Withdrawals Are Disbursed Logical Error Resolved
Description
The contract implements an expiry mechanism for withdrawal requests using function
_isWithdrawalExpired(), which returns true if a request is inRequestedorValidatedstates and its withdrawal timeout has elapsed. However,disburseWithdrawal()does not check expiry. A teller can disburse a withdrawal that has been in theValidatedstate past thewithdrawalExpiryTimeout. As a result, withdrawals that are technically expired can still be executed and paid out.Recommendation
Consider if expired withdrawals should be disbursable. If not, attempt to cancel the request when disbursing.
-
M-03 Medium Requested Funds Used As Collateral Logical Error Acknowledged
Description
When a withdrawal is requested/validated, the contract does not “lock” or put aside those funds. The user’s on-chain balance isn’t reserved since only at disbursal is
userBalancedecreased. Meanwhile, the off-chain component can still potentially treat the same balance as available margin which enables double-use of the same collateral (pending withdrawal and trading margin). This may lead to some scenarios where if a trader's position quickly turns into a loss, the Teller may disburse more funds than should be allocated to the user based on their latest position.Recommendation
Consider setting aside funds that have been requested for withdraw from being used as margin.
-
M-04 Medium Cancellations When Validated Or Disputed Logical Error Acknowledged
Description
According to technical documentation, "A user can call cancelWithdrawal(uint256 _id) to cancel their own request, but only if it is still in the Requested, Approved, or Disputed states."
Function
cancelWithdrawalexplicitly validates againstValidatedandDisputedwithdrawal requests and reverts withInvalidStateForActionif so. Consequently, user cancellation will be prevented in these states, going against technical spec.Recommendation
Either update the technical spec to reflect the latest contract behavior, or allow for cancellation in these states. Note that race conditions with off-chain component will have to be appropriately handled if cancellations are supported when the Teller can already disburse funds (in Validated state) for example.
-
M-05 Medium Lack Of Deposit Fee Warning Acknowledged
Description
The Deposit contract lacks a fee on deposits. Consequently, a malicious user may take the entire deposit capacity (the
globalMaximum) and prevent other users from depositing collateral. Depending on the size of theuserMaximum, the attacker can simply split up deposits among multiple addresses.Recommendation
Consider adding a configurable deposit fee.
-
M-06 Medium Blocked Withdraws For Non-USDT User Balance Logical Error Resolved
Description
The documentation stats that PnL is settled in USDT, so the contract must keep healthy USDT liquidity. However, the Deposit contract’s accounting treats
totalDeposited[token]as a uint that only increases on user deposits and decreases on disbursement. Trading PnL or CoW conversions that add USDT to the vault do not increasetotalDeposited[USDT].For non-USDT collateral depositors, their PnL is denominated in USDT, but no matching USDT was ever credited to
totalDeposited[USDT]. When such a user requests a USDT withdrawal, the system subtracts fromtotalDeposited[USDT]even though that counter was never incremented. This can drivetotalDeposited[USDT]below zero, trigger an underflow revert and blocking withdrawals.Recommendation
Calculate the user’s USDT PnL off-chain, price that amount into the collateral token deposited by the user (accounting for any valuation haircut as necessary), and relay the withdrawal for that amount. If USDT disbursal is needed, support a withdrawal for that amount.
-
L-01 Low Balance Validation Can Be Gamed Validation Acknowledged
Description
Function
requestWithdrawalsupports multiple withdrawal entries and validates that each entry does not exceed the contract balance.If there are multiple entries with the same token, the amount that is request for that token can exceed the actual contract balance.
For example:
(1) Contract balance = 10 WETH (2) Two withdrawal entries of 10 WETH (3) Each entry passes the validation but contract does not have enough balance to support the total withdrawal of 20 WETH.
Furthermore, the validation does not take into account tokens that have already been requested for withdrawal for a different beneficiary. This may lead to potential issues with on-chain solvency.
Recommendation
Consider adding more validation on-chain to account for requested tokens from other user withdrawal requests and/or add more off-chain validation such that each token only has one entry.
-
L-02 Low Redundant Dispute Withdrawals Superfluous Code Resolved
Description
There are multiple functions for >1 actor to dispute withdrawals:
(1)
batchDisputeWithdrawalsby RELAYER_ROLE (2)watcherDisputeWithdrawalby WATCHER_ROLE (3)batchWatcherDisputeWithdrawalsby WATCHER_ROLE (4)disputeWithdrawalcallable by RELAYER_ROLE and WATCHER_ROLEAll of this functionality can be combined into a single function.
Recommendation
Consider consolidating disputes into a single function such as
disputeWithdrawalcallable by both roles. -
L-03 Low Inaccurate User Balance Documentation Documentation Acknowledged
Description
According to documentation,
_userBalancerepresents "[t]he user's deposited amount (can be negative if losses exceed deposits)". However, according to the tests withinSuccessfulTraderWithdrawalTest, trading profit is what leads to the user balance being negative upon withdrawal disbursement. Furthermore, if a user withdraws profit so that their user balance is -X and then deposits X more tokens, their user balance appears as 0 which may cause issues with off-chain infrastructure and may bypass theconfig.userMaximumvalidation.Recommendation
Adjust the documentation to reflect trader profit leading to negative balance, or adjust the implementation so profit increases user balance.
-
L-04 Low No Subaccount Validation Warning Acknowledged
Description
There is currently no subaccount validation on the Deposit contract. For example, a user may end up depositing to a
subAccountIdthat does not exist for their address. AnAssetDepositedevent will be emitted with the non-existent subaccount, and it is crucial that the off-chain component appropriately handles this case.Recommendation
Ensure the off-chain component handles currently non-existent subaccounts.
-
L-05 Low Blacklisted Tokens Prevent Disbursal Warning Acknowledged
Description
A Teller will attempt to disburse funds through function
disburseWithdrawalafter withdrawal validation. However, if the token being transferred has blacklist functionality such as USDT and the user has been blacklisted, this will prevent the disbursal until the withdrawal turns stale and is rejected. In the meantime while the withdrawal is not yet expired, it is important that the Teller is appropriately configured so it is not prevented from disbursing other withdrawal requests.Recommendation
Ensure the Teller is capable of moving on to other withdrawal requests if a particular request is reverting. Furthermore, validate that the amount being transferred is greater than 0 within both
disburseWithdrawalandbatchDisburseWithdrawals. -
L-06 Low Lack Of Validation On Quorum Count Warning Resolved
Description
In order for a request to become validated, the
watcherQuorumnumber of votes must be reached and this parameter is configured within functionsetWatcherQuorum. ThewatcherQuorumcan be set to a value larger than the number of watches and hence the Requested -> Validated state transition will not be possible.Recommendation
Ensure the quorum amount is non-zero but no greater than the number of watchers.
-
L-07 Low Disputed Withdrawals Can Lock Out Users Warning Acknowledged
Description
Function
_isWithdrawalExpired()does not treat Disputed requests as expirable, and users cannot cancel from Disputed. Only a Guardian (or owner override) can resolve them. If Guardians (multi-sigs) are inactive, the request is stuck, and since each user may only have one active withdrawal at a time, the affected user is blocked from creating new withdrawals.Recommendation
Clearly document this risk and ensure availability of Guardians.
-
L-08 Low Lack of Minimum Withdrawal Amount Best Practices Resolved
Description
The contract enforces a minimum deposit amount with
CollateralConfig.userMinimum, but no equivalent check exists for withdrawals. A relayer can therefore create arbitrarily small withdrawal requests based on off-chain user inputs, which can lead to spam and unnecessary gas wastage.Recommendation
Consider adding the ability to set a minimum withdrawal amount.
-
L-09 Low Override Status Prevented During Pause Validation Resolved
Description
The
overrideRequestStatusfunction is intended to be used as an "[e]mergency function", but is gated by thewhenNotPausedmodifier. Consequently, if the contract is paused during an emergency, the Owner will be unable to override the status.Recommendation
Consider removing the
whenNotPausedmodifier from functionoverrideRequestStatus. -
L-11 Low User Max Not Validated With Global Max Validation Resolved
Description
The contract validates
userMaximumandglobalMaximumindependently when adding or updating collateral configs. However, it does not enforce thatuserMaximum <= globalMaximum. This allows a misconfigured token whereuserMaximumexceeds the global cap, preventing user deposits.Recommendation
Consider validating that
userMaximum <= globalMaximumwhen setting configs. -
L-12 Low Permit2 Not Supported Validation Resolved
Description
The PERMIT2 contract has the following PermitTransferFrom:
struct PermitTransferFrom { TokenPermissions permitted; // a unique value for every token owner's signature to prevent signature replays uint256 nonce; // deadline on the permit signature uint256 deadline; }which differs from SNX’s struct:
struct PermitTransferFrom { TokenPermissions permitted; address spender; uint256 nonce; uint256 deadline; }Hence the
spenderis not necessary and will trigger failure in PERMIT2 functionality when a signature is provided topermitTransferFrom.Recommendation
Update the PermitTransferFrom struct to the PERMIT2 version.
-
L-13 Low Batch Dispute Asymmetry Logical Error Resolved
Description
Function
batchDisputeWithdrawalsdoes not finalize expired withdrawal requests withStatus.Expires, which is asymmetrical withbatchRejectWithdrawalswhich does first check if a request is expired and setting its status as expired if so.Recommendation
Add a check within
batchDisputeWithdrawalsto check whether the withdrawal is expired and set it as expired if so. -
L-14 Low Watcher Votes Reuse After Dispute Logical Error Acknowledged
Description
Watcher votes are not cleared when a withdrawal request is disputed. If a request in Requested accumulates watcher votes and is then moved to Disputed, those prior votes remain stored. Should the request later return to Requested (through
overrideRequestStatusfor example), the old votes still apply, allowing the request to be quickly re-validated without a fresh round of voting. This potentially undermines the intended integrity of the dispute process.Recommendation
Consider resetting watcher votes and
watcherCountif the request moves away from a Requested state. -
L-15 Low Validation Reached Without Watcher Quorum Unexpected Behavior Acknowledged
Description
A withdrawal request can reach the Validated state without any watcher votes. Specifically, if a request in
Status.Requestedis disputed, a Guardian can callresolveDisputedWithdrawal(_approve = true), which directly transitions the request toStatus.Validated. This bypasses the watcher quorum mechanism and allows withdrawals to be validated solely through Guardian approval, which may be unexpected behavior since spec states "[a] configurable quorum (M-of-N) of Watchers must approve a request before it can be processed."Recommendation
Clearly document this behavior if intended under the GUARDIAN_ROLE section, clarifying that resolved requests can be processed for disbursal. Otherwise, transition the state to
Status.Requestedand let the watchers then cast votes. -
L-16 Low CoW Integration Trust Assumptions Warning Resolved
Description
Currently there is no validation on the CoW order within function
isValidSignaturebeyond validation that the signer is an authorized trader, leading to serious trust assumptions for authorized traders.Orders can be submitted that have an unexpected output token, where the receiver is not the Deposit contract nor an authorized receiver leading to drainage, etc. The documentation for SLP Collateral Exchange has example code including validations:
Recommendation
Consider adding more validations within function
isValidSignature. Otherwise, clearly document trust assumptions. -
L-17 Low Fee-on-Transfer Tokens Not Supported Warning Acknowledged
Description
User balance and
totalDepositedis incremented by theitem.amount, but this may differ from the actual amount received by the Deposit contract if a fee-on-transfer token was used as collateral.Recommendation
Carefully select which tokens can be used as collateral, avoiding fee-on-transfer tokens, ERC777, etc.
-
I-01 Informational Superfluous whenNotPaused Modifier Superfluous Code Resolved
Description
Function
deposituses modifierwhenNotPausedwhich revertsif (depositsGloballyPaused && withdrawalsGloballyPaused)Within the
depositfunction body,depositsGloballyPausedis specifically validated withif (depositsGloballyPaused) revert DepositsGloballyPaused();Consequently, the
whenNotPausedmodifier is not necessary since it would only be triggered ifdepositsGloballyPaused = trueanyway. The same modifier is also unnecessary on functionrequestWithdrawalsinceif (withdrawalsGloballyPaused)is validated within the function body.Recommendation
Consider removing
whenNotPausedfrom functionsdepositandrequestWithdraw. -
I-02 Informational Finalized States Repeated Throughout Contract Best Practices Resolved
Description
The boolean expression for finalized states -
req.status == Status.Disbursed || req.status == Status.Denied || req.status == Status.Cancelled || req.status == Status.Expired- is repeated throughout the contract. Consider consolidating the validation in a single function to prevent inconsistencies if future updates/upgrades occur.Recommendation
Consider consolidating finalized states into one function.
-
I-03 Informational Outdated Deposit Contract Technical Spec Documentation Acknowledged
Description
The existing Deposit contract technical doc has inconsistencies with the latest code, including but not limited to:
(1) Function
approveWithdrawaldoes not exist although in doc it is stated that "the Relayer calls this function to transition the request's status from Requested to Approved."(2)
validateWithdrawalfunction stated in technical doc does not exist and watchers cast votes withcastWatcherVoteinstead.(3) The spec mentions event
WithdrawalTimeoutSetbut the Deposit contract uses eventWithdrawalExpiryTimeoutSetRecommendation
Update the spec to match the latest Deposit contract.
-
I-04 Informational Cancel Stale Withdrawals Does Not Mark Reason Warning Resolved
Description
Function
cancelStaleWithdrawalsfinalizes the request with reason codeReasonCodes.REASON_NONE, while other cancellations on expired withdrawal requests through the Deposit contract useREASON_EXPIRED.Recommendation
Consider if
cancelStaleWithdrawalsshould set the reason code asREASON_EXPIRED.
Remediation Review
2 findings · September 18, 2025-
L-01 Low Users Can Be Trapped Upon Deposit Validation Resolved
Description
Function
updateCollateralConfigrejectswithdrawalMinimum >userMaximum, but addCollateral rejectswithdrawalMinimum >userMinimum, so after updating the collateral’s config the withdrawal floor can be raised above the deposit floor and users may be trapped.Recommendation
Use the same
if (_config.withdrawalMinimum > _config.userMinimum) revert InvalidInput();validation in functionupdateCollateralConfig. -
I-01 Informational Disburse Withdrawal NatSpec Documentation Acknowledged
Description
Function
disburseWithdrawalimplies a payout/disbursal upon successful execution, but execution can exit without disbursing anything if the underlying request is cancelled, which is not communicated in the NatSpec within ISynthetixDepositContract.Recommendation
Expand the NatSpec on
disburseWithdrawalto document that that no disbursal is expected when the request has been cancelled and thatdisburseWithdrawalcan finalize the status of the request.
Remediation Review 2
9 findings · October 8 to 12, 2025-
L-01 Low Collateral Cannot Be Added In Deploy Logical Error Acknowledged
Description
The deployment and post-deployment scripts schedule/execute
addCollateralwith only the token address, but the Deposit contract requires aCollateralConfigargument. This causes all scheduled collateral additions to revert at execution time.Recommendation
Update
addCollateralDatain DeploySynthetixDepositContract andaddDatain PostDeployment to supply aCollateralConfigrather than a token address. -
L-02 Low CowSwap Warnings Warning Acknowledged
Description
According to spec, several situations will cause the system to have to sell non-USDT collaterals back into USDT, such as full trader liquidations. When accounting for use cases such as this, it is important to note that CowSwap is not atomic and may provide partial fills.
Recommendation
Ensure the rest of the collateral exchange system is capable of supporting delayed/partial swaps.
-
L-03 Low Token Withdrawal Paused After User Requested Withdraw Warning Acknowledged
Description
A user can submit a valid withdrawal request off-chain, which will then be relayed on-chain to request the withdrawal and later disburse it. If prior to disbursal the withdrawals were paused for a particular token, the entire batch would be DoS’d.
Recommendation
Ensure off-chain simulation architecture is resilient enough to adjust
_requestIdsaccordingly. -
I-01 Informational Invalid MockToken Decimals Used For Testing Logical Error Acknowledged
Description
MockToken.mintuses the_decimalsparameter to scale the initial supply, butdecimals()is overridden to always return 6. WETH is instantiated with 18 as _decimals and a WETH-oriented config in 1e18, butdecimals()still returns 6. This inconsistency will skew deposit/limit checks and can lead to confusing or incorrect test behavior.Recommendation
Align
decimals()with the intended token decimals. -
I-02 Informational Incorrect MIN_ENVELOP_LENGTH Comment Documentation Resolved
Description
The comment for the
MIN_ENVELOPE_LENGTHis incorrect:/// 12 head words (12*32) + bytes length (32) + padded 65-byte sig (96) = 384The calculation above (
12 * 32 + 32 + 96) does not equal 384.Recommendation
Update the comment to accurately reflect the
MIN_ENVELOPE_LENGTHcalculation. -
I-03 Informational Request ID Can Be Reused Warning Acknowledged
Description
Request ID’s can be reused in multiple privileged functions when they have already been finalized. For example, the Watcher can call
castWatcherVoteson an already expired/validated request id as a no-op that will waste gas.Recommendation
Ensure the off-chain infrastructure is not reusing finalized id’s in the typical withdrawal lifecycle.
-
I-04 Informational Parameters Can Be More Granular Warning Acknowledged
Description
Multiple parameters in the Deposit contract act globally for all tokens included
slippageToleranceBps,oracleStaleTimeout, etc. It may be prudent to have more granular configurations per-token instead.Recommendation
Consider having these parameters be part of a collateral’s config.
-
I-05 Informational Specific Swap Pause Can Be Added Warning Acknowledged
Description
Considering that swaps make the
totalDepositedanduserBalancefor a token out-of-line and are one of the ways funds leave the Deposit contract, it may be prudent to have a specific swap toggle without relying on disable an entiresellToken's config.Recommendation
Consider having a pause variable specifically used in
isValidSignatureto prevent swaps. -
I-06 Informational Unclear Error InvalidTradeParams Warning Acknowledged
Description
The error
InvalidTradeParams()is raised for multiple reasons including but not limited to: slippage exceeded through swap, order buy/sell amount being 0, incorrect ordersellTokenBalance, etc. It may be better to have specific reverts for specific issues to make for easier debugging.Recommendation
Consider making reverts more specific.
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 -
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 -
SNX Vaults
49 findings2 critical · 5 high 49 findings: 2 critical, 5 high, 13 medium, 29 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.
