Guardian's review of Auto-Compounding LP Vault for Synthetix, published May 2025. The report records 80 findings across 2 review rounds, including 1 critical and 4 high.
- Published
- Review window
- April 14 to May 7, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity, TypeScript
- Chains
- Ethereum, Optimism, Base, Arbitrum
- Sector
- Perpetuals
- 1 Critical
- 4 High
- 14 Medium
- 61 Low
- 0 Informational
Scope
Findings 80
Main Review
65 findings · April 14 to 23, 2025-
C-01 Critical Unauthorized Rewards Claiming Leads To Loses Rewards Resolved
Description
RewardsDistributionRegistry.claimRewards()usesclaimRewardsFromDistributor()to claim the rewards from the Synthetix core system. Once done, the tokens will be transferred to theRewardsDistributionRegistrycontract. The rest of theclaimRewardsfunction will perform unwrapping (if needed) and will send the newly claimed amount to themsg.sender, which is expected to be theAutoCompoundLP.However, the
claimRewardsFromDistributorfunction is public which means anyone can call it, and claim the rewards.The Synthetix core will send them again to the
RewardsDistributionRegistrywith no way to get them out of there.eEven if
claimRewards()is called, as already mentioned, only the newly claimed amount will be sent to the user. In result, the rewards are stuck and the vault loses its revenue. The attack can be repeated again and again for as cheap as paying gas fees.It's expected that all users can use
RewardsDistributionRegistryto claim their rewards, not just theAutoCompoundLP, therefore they are also impacted by this vulnerability.Recommendation
Make
claimRewardsFromDistributor()internal -
H-01 High Anyone Can Update Users
userLastDepositedAccess Control ResolvedDescription
Every time a user receives
AutoCompoundLPtokens in result of a deposit, theiruserLastDepositedmapping is updated with the timestamp at which the deposit happen. This value is then used to determine two things:- whether the tokens of the user can be transferred (they can't if a given period of time hasn't passed since the last deposit time)
- calculate the amount of penalty the user should pay if they decide to redeem their tokens (the sooner the deposit, the greater the penalty)
The
AutoCompoundLP.deposit()function checks that the specifiedreceiveris themsg.senderif the function is not invoked by theLPMigrator. This protects users from becoming a target of a small deposit with the goal of reseting theiruserLastDepositedmapping.If we take a look at
LPMigrator.migrate(), which is responsible for callingAutoCompoundLP.deposit(), we will see that thereceiverpassed is never verified - it can be any arbitrary address. This allows malicious actors to bypass thereceiverrestriction in the following way:- create a brand new account with no debt and no collateral
- send a small amount of
USDCtoLPMigratorand a desiredrecipient - The
recipientreceives theAutoCompoundLPtokens and theiruserLastDepositedmapping is updated - They cannot transfer their tokens until the period passes and have to also pay the full penalty amount on redeem
These steps can be repeated again and again.
Recommendation
Don't allow passing arbitrary receiver to
LPMigration.migrate()- IAutoCompoundLP(autoCompoundVault).deposit(usdcBalance, receiver); + IAutoCompoundLP(autoCompoundVault).deposit(usdcBalance, msgSender); -
H-02 High Transfer Of SLP Shares Is Not Compatible With
ERC2771Logical Error ResolvedDescription
SNX intends to allow ERC2771 forwarders to perform transfers of SLP shares. However, since it uses its own
ERC2771Context._msgSender()implementation and invokes thetransferfunction ofERC20Upgradeablefrom OpenZeppelin—which relies on its inheritedContextUpgradeable—themsgSender()considered during the transfer is alwaysmsg.sender.As a result, all transactions initiated as meta-transactions for transfer will fail, since the transfer will be attempted from
msg.sender, i.e., the forwarder contract itself.function transfer( address to, uint256 value ) public override returns (bool) block.timestamp - userLastDeposited[ERC2771Context._msgSender()] < decayingRedemptionPenaltyDuration ) { revert TransferBlocked(); } return super.transfer(to, value); // @audit doesnt onsider context, would attempt transfer from forwarder }Recommendation
Consider overriding
_msgSender()of context upgradable -
H-03 High USDC Rewards Break The AutoCompoundingLP Unexpected Behavior Resolved
Description
RewardsDistributionRegistryhas a functionclaimRewards(), as well asgetRewards()function that returns the amount of rewards that can be claimed.Both
claimRewards()andgetRewards()have atotalClaimedvariable showing how much rewards of the given token have been claimed. It comes fromclaimRewardsFromDistributor()orclaimRewardsFromDistributor(). The amount returned and thereforetotalClaimedis always gonna be in 18 decimals precision, as we can see in the following vault logic here and here.The problem is that if
rewardIsSynth == false,rewardAmount = totalClaimedwill be executed, but all of the code afterwards assumes thatrewardAmountis in the decimals of the rewardToken.For example,
claimRewards()will revert for USDC because it tries to transferrewardAmountof USDC which is in 18 decimals, resulting in DOS of the wholeclaimRewards()function.getUsdValueOfRewards()makes a conversion by multiplying by 1e18 and dividing by 10^(rewardToken.decimals). For USDC, this results in a value with 30 decimals. This will report extremely large value toAutoCompoundLPand will completely break it.Recommendation
Convert the
rewardAmountto have the same decimal precision as the reward token on both lines:RewardsDistributionRegistry.sol:L261RewardsDistributionRegistry.sol:L165 -
H-04 High
Keeper: Swapping Unconfigured Tokens Including SNX Logical Error ResolvedDescription
The current script swaps SNX as well, which causes a spike in
totalAssetsStata, potentially enabling a sandwich attack.The SNX team does not intend to swap SNX received as rewards and instead hardcodes its price as 0 in the calculation of
totalAssetsStata.However, the keeper swaps all tokens returned from
getRewards, including SNX—even whenminandmaxare not defined for the token. The keeper simply ignores tokens without definedminandmax, but still proceeds to swap them.A further issue with this approach is that if any unexpected token is received as a reward (i.e., not currently configured), the keeper will attempt a full swap of the entire amount, potentially causing significant slippage.
Additionally, this logic can lead to an unnecessary swap attempt of
stataUSDC, which should not be swapped.Recommendation
Only swap tokens that are explicitly listed in the
config.tsfile and have bothminandmaxdefined.Log any ignored tokens.
If the ignored token is something other than SNX, trigger an alert so the SNX team can review and update the config accordingly.
-
M-01 Medium Penalty Manipulation Allows Attacker To Steal Depositor’s Funds Frontrunning Acknowledged
Description
A penalty in the
AutoCompoundLPcontract allows an attacker to manipulate the ratio of total assets to total supply, enabling an inflation attack.By performing a strategic deposit followed by a near-complete redemption (leaving only one share), the attacker can distort the asset-to-share ratio, causing subsequent depositors to receive zero shares for their deposits.
The attacker deposits a large amount and redeems all but one share, triggering a penalty and setting the total supply to 1 while maintaining total assets equal to the penalty.
This creates a state where a second depositor’s assets are absorbed without receiving any shares, as their deposit amount is insufficient to overcome the manipulated ratio and penalty.
The attacker can further dilute the ratio and redeem the assets after a decay window, potentially stealing the second depositor’s funds.
Recommendation
Consider minting dead shares equivalent to min delegation amount inside initialise.
-
M-02 Medium Migration Doesn't Check For Locked Collateral DoS Acknowledged
Description
LPMigrationwithdrawssnxUSDfrom the user's position in_withdrawAvailableSnxUsd()andsynthin_undelegateAndUnwrapSynth(). The amount to be withdrawn is fetched fromgetAccountAvailableCollateral(). However, this doesn't account for any active collateral locks.For example, user has
500available collateral, but100of them is locked, which means at most400can be withdrawn. The migrator will try withdrawing all of it (500) and the transaction will revert, resulting in inability for the user to migrate using the contract until their lock expires.Recommendation
Take into consideration the locked amount when computing how much collateral is to be withdrawn.
-
M-03 Medium
Keeper: Unauthenticated Public API Gateway Access Control Partially resolvedDescription
The POST /lp-vault-keeper endpoint is exposed without any authorizer, API-key, or IAM requirement, enabling anyone on the Internet to invoke sensitive Lambda logic.
Even though the Lambda runs in private subnets, the API Gateway lives outside the VPC; it exposes a public HTTPS URL and then calls the Lambda over AWS’s internal channel. The VPC blocks direct inbound traffic to the function, but it does nothing to hide the Gateway itself, so anyone who knows the URL can still invoke the endpoint
Recommendation
Require authentication — add a JWT/Lambda authorizer, IAM auth, or at minimum an API-key with usage-plan limits.
-
M-04 Medium Faulty USDC Delta Check Can Be Bypassed Logical Error Acknowledged
Description
The SNX team assumes that checking if
(balanceAfter - balanceBefore) < assetsReceivedis sufficient to prevent misuse in case of keeper compromise.They specifically mention this safeguard to prevent a scenario where the keeper sets themselves as the receiver:
"The main way that we saw this being possible is swapping tokens out to a separate to address. This is accounted for by the smart contract checking the returned value of the odos swap—which is the USDC received—and asserting that it equals the difference in USDC balance before and after the swap."
Notion Link
However, this assumption is flawed.
assetsReceiveddoes not validate the output token itself.A malicious keeper could perform a swap into a token with a lower value per unit than USDC, misleading the contract into believing USDC was received.
Recommendation
If the goal is to validate
assetsReceived, ensure the output token is explicitly checked.That said, even with such validations, a malicious keeper could still find other ways to extract funds, so further defense-in-depth mechanisms should be considered.
For example: M-06
-
M-05 Medium
Keeper: Reverts On USDC-To-USDC Swap Attempt Logical Error ResolvedDescription
Whenever the keeper receives USDC as rewards, it will revert.
This happens because the keeper attempts to swap USDC to USDC, and the Odos API reverts when the input token is the same as the output token.
Recommendation
Consider skipping USDC during
inputTokensfiltering. -
M-06 Medium
Keeper: Unvalidated ODOS Swaps And Private Key Exposure Risk Validation AcknowledgedDescription
The keeper script, and therefore the vault, will execute whatever swap is quoted by the ODOS API.
Since ODOS is an external dependency, blindly trusting its output is risky.
Additionally, the keeper uses an EOA.
If the private key leaks from any developer machine, the contract functions would be exposed.
Recommendation
Add validations for swap quotes returned by ODOS. These checks must execute within 60 seconds to ensure the quote does not expire.
The validation could involve simulating the
claimSwapCompoundfunction and checking the delta intotalAssetsStata. If the delta exceeds a certain threshold, the swap should be rejected.For improved key management and security, consider adding this validation for delta between before and after keeper actions at the smart contract level.
Since SNX already tracks
totalAssetsStataand does not expect large deviations from keeper actions, this validation can be incorporated easily.Implementing the validation at the contract level would also safeguard SNX in case the keeper script or its private key is ever compromised.
-
M-07 Medium
Keeper: Hard-coded Secrets & RPC Key Secret Management ResolvedDescription
SECRET_ARNand an Alchemy RPC key are committed in plaintext. Attackers with repo access can extract production secrets or drain paid API quotas.Recommendation
Store secrets exclusively in AWS Secrets Manager / SSM Parameter Store and reference them via environment variables blocked by pre-commit secret-scans.
-
M-08 Medium
Keeper: Plain-Text Private-Key Handling Secret Management ResolvedDescription
getPrivateKey()returns the raw string and downstream code could log or crash with key in memory.Recommendation
Return an AWSKMS-wrapped Signer (e.g.,
ethers.Wallet.fromEncryptedJson) and zero sensitive buffers after use. -
M-09 Medium
Keeper: No Throttling / Rate Limits DoS Partially resolvedDescription
API Gateway lacks usage plans and per-method throttling; attackers can exhaust Lambda concurrency and upstream quotas.
Recommendation
Apply a usage plan (e.g., 100 req/min) and set Reserved Concurrency on the Lambda.
-
M-10 Medium
Keeper: Lambda SG Allows All Outbound Network Egress AcknowledgedDescription
allowAllOutbound: truepermits the lambda function to connect anywhere, creating an exfiltration path if compromised and malicious code was injected into the lambda.Recommendation
Restrict egress to 443 toward approved FQDNs/CIDRs (Alchemy, Odos, AWS APIs) via prefix-lists or explicit rules.
-
M-11 Medium
Keeper: WAF Not Enabled Web Layer Protection AcknowledgedDescription
The public API is not fronted by AWS WAF, exposing it to OWASP Top-10 attacks and volumetric abuse.
Recommendation
Attach AWS WAFv2 web-ACL to the API stage; enable managed rules (
AWSManagedRulesCommonRuleSet,BotControl). -
L-01 Low Clearing Debt May DOS The Migration Rounding Acknowledged
Description
In
LPMigration._clearDebt(), theusdcAmountneeded to repay the debt is increased by 1 to account for rounding errors. Notice that this is not rounding up, it happens even if the result is a round number.Because of this, a user may end up in a situation where he has X amount of debt and X amount of
USDCin their wallet, but the code will calculate theusdcAmountasX + 1.Because of this, the X will be repaid by the user's wallet and the 1 will be taken as a flashloan.
However, the user's X is enough to repay the whole debt and as result it will be erased.
This will make
debtAmount - repayableDebt = 0, andexecuteOperation()will try selling 0 sUSD. Because thesUSD.burn()function reverts if the amount to be burned is 0, the whole transaction will be unsuccessful.Recommendation
You can implement one of the following:
- Round the
usdcAmountup only if needed - Skip the flashloan if the new debt is 0
debt = ICoreProxy(CoreProxy).getPositionDebt( accountId, poolId, collateral ); if (debt == 0) { return; } _aavePool.flashLoanSimple( address(this), $USDC, usdcAmount - userUsdcBalance, abi.encode( accountId, debtAmount - repayableDebt, debtAmount - repayableDebt, collateral ), 0 ); - Round the
-
L-02 Low Too Many Locks Can Block Migrations Informational Acknowledged
Description
LPMigrationusesCollateralModule.deposit()when clearing the positive user debt. This action processes all locks of the user (999999999999999is used ascountforcleanAccountLocks). Because of this, accounts with too many locks may require too much gas to be migrated, potentially even blocking the migration.Recommendation
Inform users about locks increasing the cost of the migration.
-
L-03 Low
stataUSDCShould Not Be Added As Reward Warning AcknowledgedDescription
AutoCompoundLP.totalAssetsStata()adds the value of the reward tokens held by that contract towards all the available assets. ShouldstataUSDCbe added as a reward tokens,_convertToAssetsStatacalculations will return wrong results. For example, when users deposit, theirUSDCis turned intostataUSDCand then_convertToAssetsStata()is called. HavingstataUSDCitself as a reward token will lead to inflation in the amount of available assets and thus the user receiving wrong amount.Recommendation
Don't add
stataUSDCas reward token for as long as the code performs the assets calculation like it does now. -
L-04 Low
totalAssetsStataCan Be Slightly Inflated Rounding AcknowledgedDescription
AutoCompoundLP.totalAssetsStata()adjusts the available assets amount by converting the absolute value ofnetNonNativeAmounttostataUSDCby dividing by therate().In cases when
netNonNativeAmountis negative, themulDivwill round the result up. For example, if the debt is -5.5, the function will round it as 5, not 6 and in resulttotalAssetsStatawill be inflated.Recommendation
Consider rounding in the opposite direction if
netNonNativeAmountis negative. This will cause precision loss in the other direction, but it's generally better to overestimate loses compared to underestimating them. -
L-05 Low WrapperModule Rounding Issues Rounding Acknowledged
Description
WrapperModule.wrap()andWrapperModule.unwrap()take an amount of tokens towrap/unwrap, applies a fee to it and returns the result in the appropriate token. For the scope of this audit, the fees are assumed to always be 0. However, let's look at the way the amounts are computed.uint256 usdAmount = baseAmountD18.mulDecimal(synthPrice); synthAmount = usdAmount.divDecimal(synthPrice);These 2 lines are taken from
wrap, but they are the same inunwrap(the names are different). Let's replacemulDecimalanddivDecimal. Then we gotuint256 usdAmount = baseAmountD18 * synthPrice / 1e18; synthAmount = usdAmount * 1e18 / synthPrice;Because of the first division,
usdAmountcan experience a loss up to (but not including)1. This loss is then amplified by the multiplication with1e18 / synthPrice, resulting in a maximum possible loss of up to floor(1e18 / synthPrice+ 1) (+1 because of the division on the second line). ForsynthPrice > 1e18the maximum loss should be capped to1, but the lower the price is, the bigger the potential precision loss experienced.To handle that,
RewardsDistributionRegistry._unwrappedAmount()subtracts 1 from the expected received amount. As mentioned above, this should be fine forsynthPrice > 1e18, but can result in reverting transactions if the price drops below that.Now let's look at the wrap() that happens in
_increasePosition. The synth market used is thesynthIdStataUSDC. ThesynthAmountexpected to be received doesn't account for the precision loss. At the moment of writing, indexPrice of that market returns a value which has its last 12 digits as 0s. If this property stays true, then there is no need to account for the precision loss because the convertedsynthIdStataUSDCwill also has its last 12 digits as 0s which means there is no precision loss at all. However, if the price were to change its format, then the code should account for that by adjusting the slippage tolerance as well.There are 2 more types of
wrap/unwrapped()that haven't been discussed:unwrap synthIdStataUSDC- there is no need to account for the precision loss here because after it happens, the amount is divided by1e12. This means the part where the precision loss happened is irrelevant. Keep in mind that if the decimals of thestataUSDCchange (highly unlikely), you may need to adjust the expected returned amount as well.wrap/unwrap synthIdUSDC- the price of this market is hardcoded to1e18, which means that when the 2 lines of code in this report are executed, there won't be a precision loss.
Recommendation
Consider if any of the behaviors mentioned in the report should be taken into account and if so, change the slippage protections accordingly.
-
L-06 Low Loss Of Rewards From Removed Distributor Rewards Acknowledged
Description
When
removeDistributor()is called, it will remove the distributor fromdistributorDataanddistributors. This will make any rewards that have accrued from that distributor and not been claimed yet to be lost. This happens because distributors loop throughdistributors, and will no longer be able to claim them.Recommendation
Prior to deleting the data for a distributor, claim the rewards from that distributor first from
AutoCompoundLP.sol. -
L-07 Low DoS Due To Synthetix
maxWrappableAmountLimit DoS AcknowledgedDescription
The
AutoCompoundLPcontract's core functions,depositandcompound, rely on the internal_increasePositionfunction. This function wrapsstataUSDCintosynthStataUSDCby callingIWrapperModule.wrapon the Synthetix Spot Market (spotMarketProxy).The Synthetix
wrapfunction enforces a market-widemaxWrappableAmountlimit for each specific synth market. ForsynthStataUSDC(market ID3), this limit is currently set at 50 million units, represented as50000000000000000000000000(5e25) when scaled to 18 decimals, according to Synthetix V3 configuration (e.g., found inmeta.jsonwithin node modules).If this market-wide limit (
maxWrappableAmount) is reached due to aggregate wrapping activity across all protocols and users interacting with the SynthetixsynthStataUSDCmarket, subsequent calls towrapfor market ID3will revert.This reversion will directly cause failures in:
deposittransactions initiated by users attempting to add liquidity to theAutoCompoundLPvault.compoundtransactions initiated by keepers attempting to reinvest rewards. This, in turn, can causeredeem()to revert. This will occur because the calculation for assets takes the rewards into consideration. Therefore, a user may get an overestimated amount of USDC as a reward, that can not be sent to them due tocompound()reverting.
This leads to a Denial of Service (DoS) for these essential vault operations. The
AutoCompoundLPvault's ability to accept new deposits and to auto-compound rewards becomes entirely dependent on the available capacity within the external Synthetix protocol's wrapper limits forsynthStataUSDC.Recommendation
If this limit proves to be a frequent bottleneck for the
AutoCompoundLPvault's operation, the recommended long-term solution is to engage with Synthetix governance to request an increase in themaxWrappableAmountfor thesynthStataUSDCmarket (ID3).Furthermore prepare communication strategies for users and keepers should the limit be approached or reached, explaining why
depositorcompoundoperations might be temporarily unavailable due to the external Synthetix limit. -
L-08 Low Loss Of Rewards When Updating
rewardsRegistryRewards AcknowledgedDescription
This
AutoCompoundLP.setRewardsRegistryis anowner-onlyfunction allows changing the address of therewardsRegistrycontract. TheAutoCompoundLPvault uses this registry to claim and value Synthetix rewards associated with its SynthetixaccountId. The function performs two main actions:- Updates the
rewardsRegistrystate variable to thenewRegistryaddress. - Grants the necessary
REWARDS_PERMISSIONwithin the Synthetix system to thenewRegistryaddress for the vault'saccountId.
The
setRewardsRegistryfunction lacks a mechanism to ensure that any pending rewards, claimable only through the oldrewardsRegistryaddress, are claimed before the switch to thenewRegistryoccurs. These pending rewards will become permanently inaccessible (if all the old reward distributors are not registered in the newRegistry) via the vault's standard functions listed below:claimRewardsclaimSwapCompoundtotalAssetsStata(which internally callsgetUsdValueOfRewards)
This scenario leads to a potential permanent loss of funds for the vault and its depositors, as the unclaimed rewards become stranded.
Recommendation
To prevent the potential loss of accrued rewards during a registry update, it is recommended to claim the pending rewards of the old
rewardsRegistry, before the owner callssetRewardsRegistry. - Updates the
-
L-09 Low DoS In Reward Claiming Due To Strict Slippage DoS Acknowledged
Description
AutoCompoundLP.sol::claimSwapCompoundcallsRewardsDistributionRegistry.sol::claimRewardsto process rewards.For synthetic rewards (
rewardIsSynth=true),claimRewardsloops through distributors and calls_unwrapSynth. This function uses_unwrappedAmountto determine the minimum underlying tokens expected (minAmountReceived) for theIWrapperModule.unwrapcall._unwrappedAmountfunction calculates this by subtracting exactly1 weifrom the expected amount, aiming to cover rounding errors. Crucially, this calculation does not factor in theunwrapFixedFeespecific to the Synthetix spot market (spotMarketId) being used for that distributor.If any distributor's market has an
unwrapFixedFeeconfigured, theunwrapcall will receiveexpectedAmount - fee, which is less than the requiredminAmountReceived(expectedAmount - 1 wei). This triggers a revert withinIWrapperModule.unwrapdue to failed slippage protection.Because this check occurs inside the
claimRewardsloop, the failure of even a single distributor's unwrap reverts the entireclaimRewardsfunction call. Consequently, the calling functionclaimSwapCompoundalso reverts. This leads to a Denial of Service (DoS) vulnerability, preventing keepers from successfully claiming and compounding any rewards system-wide until the problematic distributor is removed or its associated market fee configuration changes.Recommendation
Modify the
_unwrapSynthfunction (or its helper_unwrappedAmount) withinRewardsDistributionRegistry.solto correctly calculate theminAmountReceivedpassed toIWrapperModule.unwrap, ensuring it accounts for potential fixed unwrap fees. This can be done by querying theminAmountRecieved amountby calling thequoteUnwrapfunction of the respectivespotMarket. -
L-10 Low Unused Token Approvals To ODOS Pose Security Risk Logical Error Acknowledged
Description
As per known issues for the vault, SNX is aware that
performSwapsapproves tokens to the ODOS router even if they are not intended to be swapped—specifically SNX, stataUSDC, and USDC.SNX’s position is that the keeper would never attempt to swap these tokens.
However, DEX routers have previously been compromised to exploit unused allowances.
So even if the keeper does not initiate swaps for these tokens, they could still be maliciously swapped with different receivers if the router is compromised.
Recommendation
Only approve tokens that are actually required for the intended swap.
-
L-11 Low Old Rewards Registry Retains Permissions After Replacement Logical Error Acknowledged
Description
SNX allows the owner to update the rewards registry if needed.
However, when this change occurs, the
REWARDS_PERMISSIONfor the old rewards registry is not revoked.Recommendation
Consider revoking
REWARDS_PERMISSIONfrom the old rewards registry upon updating to a new one. -
L-12 Low AAVE Deposit/Redeem Limits May Affect stataUSDC Operations Error-Handling Acknowledged
Description
SNX uses
stataUSDCas collateral for this vault’s account ID.Each user’s USDC deposit is sent to AAVE, which returns
stataUSDC. This token is then deposited and delegated to SNX.During redemption,
stataUSDCis retrieved from SNX Core and then redeemed back to USDC.Debt repayment also includes the redemption of
stataUSDCto USDC.However, AAVE enforces both
maxDepositandmaxRedeemlimits:These limits could potentially cause reverts during user interactions.
Recommendation
While current AAVE limits are unlikely to affect SNX operations, it's important to monitor them.
As TVL grows or if AAVE modifies its caps, vault actions like deposit, redeem, or debt repayment could begin reverting.
Be aware of this possibility and consider implementing safety checks or fallback mechanisms.
-
L-13 Low Vault Deviates From ERC4626 Warning Acknowledged
Description
SNX does not anticipate trading activity involving trading shares, so this is not currently a major issue.
However, both the SNX team and any integrators should be aware that there are different representations of
totalAssets.The cached value may not reflect the current state of the vault.
For any external actions involving the vault, depending on the share value, either
syncfunctions should be used orrefreshTotalAssetsCacheshould be called.Integrations should be especially cautious when choosing which method to use, and should not assume full ERC4626 equivalence, as this vault implementation is not fully compliant.
Recommendation
N/A – This is added as a reference for integrators to consider during implementation.
-
L-14 Low RewardsDistributionRegistry Lacks Flexibility Warning Resolved
Description
The
RewardsDistributionRegistryhas the USDC value hardcoded as1e18.This can lead to inaccurate share valuations if USDC depegs, as it did briefly in the past.
While SNX’s
OracleManagernodes also configure USDC as aCONSTANTnode (valued at1e18), this can be updated to use a real Pyth price feed if needed.However, this flexibility does not exist in the
RewardsDistributionRegistry, where the value is fixed.Recommendation
Consider fetching the USDC price from
coreProxyas well, to ensure dynamic and accurate pricing. -
L-15 Low Issues With Reward Tokens Array Handling Unexpected Behavior Acknowledged
Description
The current approach in
RewardsDistributionRegistryis to add each synth reward underlying token in therewardTokensarray and map it to the synth in therewardTokenToSynthTokenmapping.This is done only if the reward token is not already added to the array. Even if the distributor is removed, these data structures are not changes. There are several security consideration around this behavior:
- the call to
getCollateralPrice()is not wrapped in atry/catch, which means if it reverts for an old token,usdValueOfCallerBalance()will be DOS-ed - If the same reward token is added with a different synth, the
rewardTokenToSynthTokenmapping won't be updated and it will keep pointing to the old synth - If the array becomes too long, looping over it may cause OOG revert
Recommendation
Reconsider the approach to never delete these values on distributor removal
- the call to
-
L-16 Low Penalty Rounding In Favor Of The User Rounding Resolved
Description
The
decayingRedemptionPenaltyfunction calculates the penalty amount to be deducted during a redemption. The final calculationassets.mulDiv(timeAdjustedFeePercent, PERCENT_PRECISION)uses the default behavior ofmulDiv, which rounds the result (the penalty amount) down.Rounding the penalty amount down slightly benefits the withdrawing user, as they pay a smaller penalty than if rounding were neutral or upwards. This contradicts the rounding approach potentially used elsewhere, such as when converting shares to assets (e.g.,
_convertToAssetsStatausesMath.Rounding.Floor), which explicitly rounds the user's entitled assets down, favoring the protocol.Recommendation
To ensure rounding consistently favors the protocol during redemptions involving penalties, modify the final calculation within
decayingRedemptionPenaltyto use ceiling division that rounds the penalty amount up. -
L-17 Low Use
safeApprove()&safeTransfer()Best Practices AcknowledgedDescription
The protocol uses
transfer()andapprove(), instead ofsafeTransfer()andsafeApprove(). Some ERC-20 tokens have custom logic, such as USDT on ethereum mainnet’s approvals must be reset to zero before creating a new approval. This can lead to unexpected reverts throughout the protocol.Recommendation
Switch all instances of
transfer()andapprove()tosafeTransfer()andsafeApprove(). -
L-18 Low
Keeper: Missing Validation On Rewards Return Value Validation ResolvedDescription
The return value of the rewards function is not validated, which could result in an empty rewards list being passed to
inputTokens.Recommendation
Consider adding validation to check the return value of the rewards function.
-
L-19 Low
Keeper: getRewards Skips Tokens Held In Vault Balance Logical Error ResolvedDescription
The
getRewardsfunction only returns newly claimable rewards, and does not account for tokens already held in the vault's balance.For example, if token A had rewards in the last epoch and was partially swapped—leaving the rest in the vault for periodic swapping—then in the next epoch, if
claimableRewardsfor token A is zero, it won’t be included ingetRewards. As a result, the keeper won’t attempt to swap it.Recommendation
Consider iterating through the internal rewards array in the keeper script and logging the vault's balance of each token during
populateTokenAmounts. -
L-20 Low
Keeper: Incomplete Error Handling Error-Handling ResolvedDescription
The current
getOdosQuoteerror handling is incomplete for responses with status codes 422 and 502, as these do not return anerrorCodein the expected schema.Recommendation
Consider checking for a successful response explicitly, rather than relying on the absence of failure. Revert if a successful return is not detected
-
L-21 Low
Keeper: assembleOdosQuote May Use Expired Or Invalid Quotes Error-Handling ResolvedDescription
The
assembleOdosQuotefunction lacks error handling and does not check for deprecation status.This could lead to incorrect or expired calldata being used in execution.
Recommendation
Consider adding proper error handling in
assembleOdosQuote, including a check to ensure the quote is not marked as deprecated (the first field in the response). -
L-22 Low
Keeper: No Monitoring Of Keeper ETH Error-Handling AcknowledgedDescription
There is no tracking for the keeper's ETH balance or the gas used.
This could result in keeper transactions failing due to insufficient funds for gas.
Recommendation
Consider adding logs for gas used and remaining ETH balance, along with an alert system to notify admins if the balance falls below a defined threshold.
-
L-23 Low
Keeper: Non-Checksummed Addresses Logical Error ResolvedDescription
ODOS requires all addresses to be checksummed according to their API documentation:
Parameter Description Required tokenAddressAddress of the token to swap from. This should be a checksummed address. Yes Currently, SNX does not have the following addresses checksummed in
config.ts, which could result in reverts:0x833589fcd6edb6e08f4c7c32d4f71b54bda029130xc1cba3fcea344f92d9239c08c0568f6f2f0ee452
Recommendation
Review all addresses used in the keeper infrastructure and ensure they are properly checksummed where required.
-
L-24 Low
Keeper: Potential Sandwich Risk Warning AcknowledgedDescription
SNX currently uses Alchemy as their RPC provider.
This setup could become vulnerable to sandwich attacks if Base implements a public mempool with bribes, replacing the current FCFS (First-Come, First-Served) sequencer model.
Recommendation
Once Base introduces a public mempool with bribe support, consider switching to a frontrunning-resistant RPC.
-
L-25 Low
Keeper: Default Slippage May Misprice Reward Logical Error ResolvedDescription
Since SNX does not use a slippage configuration in
getOdosQuote, ODOS applies its default slippage of 0.3%.This may be either too high or too low depending on the specific reward token, potentially leading to suboptimal or failed swaps.
Recommendation
Consider adding a slippage parameter in
config.tsfor each reward token and using it when requesting quotes from ODOS. -
L-26 Low Migration Omits Synthetix Reward Claiming Rewards Acknowledged
Description
The
migratefunction inLPMigration.solis designed to unwind a user's Synthetix V3 position (collateral and debt) and deposit the net USDC value into theAutoCompoundLPvault. However, the current implementation only handles the principal collateral ($synthUSDC,$synthStataUSDC) and associated debt (snxUSD). It completely omits the process of claiming any pending Synthetix rewards accrued by the user'saccountIdfor providing liquidity or collateral.Consequently, the value of these unclaimed rewards is not included in the initial deposit made to the
AutoCompoundLPvault during the migration. The user must manually claim these rewards later and deposit them separately.This creates two primary issues:
- Incomplete Migration & User Burden: The migration is not comprehensive, requiring extra steps from the user.
- Missed Compounded Yield: The core problem is the delay in depositing the reward value into the
AutoCompoundLPvault. During this delay, the reward value is not participating in the vault's yield generation strategy (Aave yield + vault's own compounded Synthetix rewards). When the user eventually deposits the reward value, the vault's share price will likely have appreciated due to accrued yield, resulting in the user receiving fewer shares for their rewards than if deposited initially. This represents a loss of potential compounded yield within the target vault for the user.
Recommendation
Enhance the
migratefunction inLPMigration.solto include the claiming and processing of Synthetix rewards of the migrated position, before depositing into theAutoCompoundLPvault. -
L-27 Low Rewards Discount Impacts Share Calculations Informational Acknowledged
Description
AutoCompoundLP.totalAssetsStata()appliesrewardsUsdValueDiscountto the rewards available to account for fees and slippage when swapping these tokens. Any deposits and redeems executed before the swap of the reward will use the new ratio which includes the discount. Any leftover amount of the discount which was not used for fees will be distributed across the vault participants at the time of the swap, resulting in a profit for the new depositors.Recommendation
There is no need for a fix because any amount not used for fees can be considered as a bonus, so documenting the issue and keeping it in mind is enough.
-
L-28 Low Tokens With Invalid Prices Are Treated As 0 Unexpected Behavior Acknowledged
Description
RewardsDistributorRegistry.registryDistributor()doesn't allow adding a distributor if the token it's responsible for doesn't have a valid price, i.egetCollateralPrice()reverts or returns 0.However, the
_getAssetPrice()function returns the result ofcoreProxy.getCollateralPrice(assetAddress)as price of the requested asset without doing any checks. If an error occurs at a later point in time insidegetCollateralPrice, the returned value will be 0 and the given reward token will be treated as 0.This can lead to unexpected behaviors since users can still interact with the
AutoCompoundLP, but these rewards tokens will be ignored causing spikes in the value of the total assets.Recommendation
Create a plan for what to do in such emergency cases and implement any changes if needed. Keep in mind that reverting is risky because once a token is added in
rewardTokens, it stays there forever. -
L-29 Low Unconditional Rounding In
_repay()Rounding AcknowledgedDescription
There is
1added tousdcAmountinAutoCompoundLP._repay()to handle rounding issues.However, the 1 is added unconditionally which creates problems for both
redeem()andcompound()because there the amount ofUSDCneeded to repay the debt is computed withmulDivandMath.Rounding.Ceilwhich round the number up only if the mathematical result of the division is not a whole number.For example, consider the case where there is
1e18debt of theAutoCompoundLPposition and a user tries toredeem. The code will take a flash loan of1e6USDC inredeem(), but will try to wrap1e6 + 1because of this+1happening, resulting in reverting redeem transaction. The same is true for thecompound()function.Recommendation
Replace the rounding with
mulDiv.After that, you can delete the following -1
-
L-30 Low Cached Assets Don't Account For Redeem Penalty Math Acknowledged
Description
When users redeem their
AutoCompoundLPshares,_convertToAssetsStata()will update thetotalAssetsCachetocurrentTotalAssets - assets. Hereassetsis the requested amount to be redeemed, but it doesn't include the penalty amount which technically doesn't leave the system and should not be subtracted. This results in a discrepancy between the real USDC value and what's cached and in turn leads to wrong results returned frompreviewDepositandpreviewRedeem().Recommendation
You can make the
refreshTotalAssetsCache()visibility public and use it throughout the codebase to refresh the cache. -
L-31 Low Liquidations Break The Vault Unexpected Behavior Acknowledged
Description
The
AutoCompoundLPvault is a participant in the v3 core system which means it's a subject to liquidations if its collateral ratio becomes unhealthy. If that happens, both its debt and collateral will be erased, but the vault depositors will continue holding all their previous tokens. If the vault remains active after the liquidation, the old depositors will have exposure to all the funds deposited afterwards, leading to loss for the new depositors and profit for the old ones.Recommendation
Consider halting the vault in case its experiences a liqudiation.
-
L-32 Low Donator Can Undelegate Vault Collateral Informational Acknowledged
Description
When calculating the worth of the
AutoCompoundLPshares, the reward tokens held in the contract are also included intotalAssetsStata. This allows a vault participant to donate a large amount of rewards to the vault, change the share price and then redeem their shares for a large amount of the current delegated assets, leaving the vault with onlyminDelegationAmountor even0assets.Doing this is expensive since the more assets they are in the vault, the more rewards the user has to donate, but it can lead to some unexpected behaviors, like the
AutoCompoundLPnot receiving rewards and liquidation rewards or depositors not being able to redeem because there is not enough liquidity.Recommendation
Document this behavior.
-
L-33 Low Locked Market Can Block Redeem Informational Acknowledged
Description
When users redeem, if there is debt accrued by the
AutoCompoundLPposition, it will be repaid with an Aave flashloan.Then the amount undelegated from the position is the amount to be received from the user + the amount to repay the flashloan.
This can cause the amount delegated to the market to fall below its
minimumCreditand in result the redeem will be unsuccessfull.Recommendation
Document the risk.
-
L-34 Low Unnecessary Reward Operation Informational Acknowledged
Description
RewardsDistributionRegistry.claimRewardsFromDistributor()andRewardsDistributionRegistry.getRewards()callclaimPoolRewards()andgetAvailablePoolRewards()twice - the first time for thelpCollateralAssetvault and the second time for theaddress(0)vault. Usually, it's not expected to have active stakers in the vault foraddress(0)collateral, which makes the second call unnecessary.Recommendation
Consider whether the second call is necessary and remove it otherwise.
-
L-35 Low Alchemy API Key Exposed Warning Resolved
Description
The API key for your Alchemy account is exposed in your test cases. This can allow anyone to use your API and make calls from your account.
Recommendation
Use a
.envfile to store the API key. Then concatenate a string with the base of the URL with the API key. -
L-36 Low
Keeper: Dead Code / TODOs Best Practices AcknowledgedDescription
Functions like
getRandomVariance()andsubmitTransaction()are stubs; TODOs indicate unfinished logic that can mask security gaps.Recommendation
Remove or complete TODOs; run static analysis to ensure no unreachable code remains.
-
L-37 Low
Keeper: Hard-coded Resource Identifiers Best Practices AcknowledgedDescription
Token lists, chain IDs, and account-specific constants are fixed in code, causing config drift across environments.
Recommendation
Externalise to SSM parameters/CDK context and document overrides for staging vs. prod.
-
L-38 Low
Keeper: Non-optimal Lambda Sizing Best Practices AcknowledgedDescription
memorySize:256 MBwithtimeout:30 scould be over- or under-provisioned, affecting cost and latency.Recommendation
Benchmark under load; right-size memory (which also boosts CPU) and reduce timeout to ≤10 s if feasible.
-
L-39 Low
Keeper: Opaque Cross-Stack Imports Best Practices AcknowledgedDescription
Subnet IDs/security group IDs are imported via CloudFormation outputs without tags or documentation, hindering troubleshooting.
Recommendation
Tag imported resources (
cdk.Tags.of(...)) and maintain design-docs/diagrams that map stack dependencies. -
L-40 Low
Keeper: Sensitive Request/Response Logging Best Practices AcknowledgedDescription
dataTraceEnabledis true (non-prod), writing full request/response bodies—including tokens & calldata—to CloudWatch.Recommendation
Disable full traces or mask fields with log-filtering. Encrypt logs and limit reader IAM roles.
-
L-41 Low
Keeper: Short Log Retention Best Practices ResolvedDescription
Logs are retained for ONE_WEEK — insufficient for many compliance or forensic requirements.
Recommendation
Extend retention to ≥90 days or export to S3 + Glacier automatically.
-
L-42 Low
Keeper: Inefficient Secret Retrieval Best Practices ResolvedDescription
SecretsManager.getSecretValue()is executed on every invocation, adding latency and cost.Recommendation
Cache the secret in /tmp (persist across warm invocations) or use Secrets Manager extension.
-
L-43 Low
Keeper: RemovalPolicy DESTROY On Logs Best Practices ResolvedDescription
RemovalPolicy.DESTROYdeletes log groups when the stack is torn down, erasing evidence needed for investigations.Recommendation
Set
RemovalPolicy.RETAINin production and automate log archival to S3 before stack deletion. -
L-44 Low
Keeper: External Calls Lack Timeout & Retry Best Practices ResolvedDescription
fetch()to Odos/Alchemy has no timeout or retry; slow responses lead to Lambda timeouts.Recommendation
Wrap calls with AbortController (≤5 s) and implement exponential back-off with jitter.
-
L-45 Low
Keeper: Missing Input Validation Validation ResolvedDescription
Proxy integration forwards raw payloads; the Lambda never validates schema or data types, enabling injection or resource-exhaustion attacks.
Recommendation
Add strong JSON schema validation (e.g.,
ajv) and early-return400on malformed requests.if(!event.body) return {statusCode:400,body:'Missing body'}; -
L-46 Low
Keeper: Over-Permissive / Implicit Lambda Role IAM ResolvedDescription
No explicit IAM role is attached; CDK’s default role may include permissions the function doesn’t need or omit required ones.
Recommendation
Define a custom least-privilege role
const lambdaRole = new iam.Role(this,'Role',{assumedBy:new iam.ServicePrincipal('lambda.amazonaws.com')}); lambdaRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')); lambdaRole.addToPolicy(new iam.PolicyStatement({actions:['secretsmanager:GetSecretValue'],resources:['*']})); -
L-47 Low
redeemRevert Due To Flash Loan Underfunding DoS AcknowledgedDescription
The
redeemfunction calculates the amount ofUSDCneeded for an Aave flash loan (usdcNeededToRepay) to repay the user's Synthetix debt (debt). This calculation is performed by theusdcToRepayDebthelper function.Currently,
usdcToRepayDebtperforms a direct 1:1 decimal conversion from the 18-decimaldebt(denominated insnxUSD) to 6-decimalUSDC. However, the actual repayment happens within the Aave flash loan callback (executeOperation), specifically inside the_repayinternal function. This function determines the precise amount ofsynthUSDCneeded to acquire the targetdebtAmountofsnxUSDby querying the Synthetix market viaIAtomicOrderModule.quoteSellExactOut.Crucially, the quote returned by
quoteSellExactOutincludes potential positiveskewFeesimposed by the Synthetix market if the trade would increase market imbalance.If a positive
skewFeeis applied by Synthetix during thequoteSellExactOutcall, the requiredsynthAmount(and consequently theusdcAmountneeded in_repay) will be slightly higher than the simple 1:1 conversion amount calculated initially byusdcToRepayDebtwhen determining the flash loan size (usdcNeededToRepay). As a result the contract will lack sufficientUSDCreceived from the flash loan to perform the necessarywrapoperation inside_repay.This results in the entire
redeemtransaction reverting due to insufficient funds during the flash loan callback execution.Recommendation
To guarantee the flash loan always covers the required repayment amount, including any potential Synthetix
skewFees, modify theredeemfunction to calculateusdcNeededToRepaybased on a real-time quote from the Synthetix market before initiating the flash loan. Else recommended to apply enough buffer on theusdcNeededToRepayvalue before calling theflashloansuch that the_repaywill not revert due to insufficient funds. -
L-48 Low
Keeper:Outdated AWS CDK And Deprecated SDK Best Practices ResolvedDescription
In the
lp-vault-keeper.tsfile, the script relies onaws-cdk-lib.However, the version used (as per
package.json) is2.186.0, which is affected by the 2L/1M vulnerability.Additionally, the
lp-vault-keeperpackage listsaws-sdkas a dependency. This SDK has been in maintenance mode since September 2024 and will no longer receive full support beyond September 2025.This introduces long-term maintenance and potential security risks, especially for components relying on AWS services like Cognito, IAM, or Lambda integrations.
Recommendation
- Upgrade
aws-cdk-libto the latest secure version (e.g.,2.191.0as of the time of this review). - Migrate from
aws-sdk(v2) to the modular v3 (@aws-sdk/*) packages, as recommended by AWS.
This ensures continued compatibility, improved performance, and access to ongoing security patches.
- Upgrade
-
L-49 Low
Trust AssumptionsInformational AcknowledgedDescription
RewardsRewards Registry Decimal Precision
The current version of the rewards registry is not expected to handle reward tokens with fewer than 18 decimals. All configured synth rewards are standardized to 18 decimals.
Pricing, Oracles & PrecisionConversion Ratios
Wrap/unwrap operations for USDC, snxUSD, and sUSD are assumed to follow a 1:1 value ratio with no fee.
However, minor rounding deltas are known and are currently handled via manual
+1/-1adjustments in logic.Oracle Pricing and Staleness Risk
The price of synths is derived from their underlying collateral via Pyth oracles.
It is assumed prices will generally remain fresh, as SNX uses a keeper that updates all markets hourly and key assets (e.g., ETH, BTC) at price thresholds.
However, temporary staleness remains a valid risk.
sStataUSDC Precision Assumption
It is assumed that the price of
sStataUSDCwill consistently end in 12 zeros (fixed-point format).This has been validated through testing but is not guaranteed. Deviation could result in rounding or precision loss during conversions.
Keeper Behavior & ManagementTrusted Keeper Execution
It is assumed that the keeper will consistently and honestly perform its upkeep duties.
Secure Keeper Key Management
The keeper operates as an EOA. It is assumed that key access is tightly controlled and will not be misused by insiders or compromised from developer machines.
Low Risk of Insider Sandwiching Attacks
It is assumed that knowledge of pending compound events will not lead to effective sandwich attacks.
This is mitigated by randomized execution timing and Odos routing.
Randomized Keeper Timing Prevents Predictability
Random variance in keeper execution is assumed to reduce predictability and MEV risks during compound swaps.
Swap Size Limits Prevent Exploitable Volume
Maximum swap sizes ensure that large swaps are broken into smaller chunks, reducing the risk of slippage or sandwiching attacks.
Separation of Claim, Swap, and Compound Logic
These actions are modular in the keeper, allowing finer control and flexibility to avoid swapping dust tokens or executing unneeded compound steps.
Vault Entry/Exit & User BehaviorWithdrawal Penalty Discourages Quick-In/Quick-Out
A decaying withdrawal penalty (e.g., 1% over 1 day) is assumed to discourage users from quickly entering and exiting the vault in response to large trader liquidations.
24-Hour Transfer Lock Prevents Penalty Circumvention
Vault shares are non-transferable for 24 hours after deposit.
This is assumed to prevent circumvention of withdrawal penalties via share transfer to another address.
Timeout Overrides During Migration
- For the LP vault,
_CONFIG_SET_ACCOUNT_OVERRIDE_WITHDRAW_TIMEOUTwill be used. - For migration contracts,
_CONFIG_SET_SENDER_OVERRIDE_WITHDRAW_TIMEOUTis planned (not yet deployed on Base V3).
These allow per-user or per-sender timeout bypassing when needed.
TotalAssetsSync Includes Uncompounded Rewards
Uncompounded rewards are reflected in
totalAssetsSync.It is assumed this may introduce temporary valuation mismatch during mass redemptions, but is acceptable due to flexibility in triggering compounding.
Partial ERC4626 ComplianceDeviation from ERC4626 Interface
The vault does not fully implement the ERC4626 standard.
Functions such as
totalAssetsmay require state-changing operations and are not strictviewfunctions.It is assumed that integrators are aware of this and will use the provided alternatives (e.g.,
totalAssetsSync) instead of expecting strict interface conformance.Recommendation
Beware of these trust assumptions, a change in any of these may require a code change.
- For the LP vault,
Remediation Review
15 findings · May 6 to 7, 2025-
M-01 Medium Stuck Rewards in
RewardsDistributionRegistryLogical Error AcknowledgedDescription
The
claimRewardsFromDistributorfunction inRewardsDistributionRegistry.solis currentlypublic. While an access control check (if (msg.sender != coreProxy.getAccountOwner(accountId)) revert UserNotAccountOwner();) was added to ensure only the account owner can initiate the claim from the SynthetixcoreProxy, severe issue remains.If an account owner calls
claimRewardsFromDistributordirectly, the rewards are claimed from thecoreProxyand are held by theRewardsDistributionRegistrycontract. However, the primaryclaimRewardsfunction is designed to process and transfer rewards that are claimed during its own transaction execution.Specifically,
claimRewardsiterates through distributors, callsclaimRewardsFromDistributorfor each, and then transfers thetotalClaimedamount (which is the result of this specific internal call) tomsg.sender. IfclaimRewardsFromDistributorwas called directly in a previous, separate transaction, the rewards from that initial call are now sitting in theRewardsDistributionRegistry. WhenclaimRewardsis subsequently called,getRewardsFromDistributorwill likely return 0 for that distributor.As a result, the rewards claimed through a direct, prior call to the public
claimRewardsFromDistributorfunction are never transferred to the user by theclaimRewardsfunction and become permanently stuck in theRewardsDistributionRegistrycontract.Recommendation
Move the check back to the old function (for gas savings) and change the visibility of
claimRewardsFromDistributortointernal. -
M-02 Medium
KeeperIneffective /tmp Caching Logic Causes Crash on Cold Start IAM AcknowledgedDescription
SNX introduced a
/tmpcheck so the Lambda looks forprivate_key.txtandrpc_url.txtbefore calling Secrets Manager. That’s the right idea, but the implementation is incomplete.What SNX did:
–
getWalletandgetRpcUrlread the file in/tmp; if it exists, they use it. Otherwise, they fall back to Secrets Manager.Remaining problems:
- The initial
fs.readFileisn’t wrapped intry-catch. On the first cold start, the file won’t exist,fsthrowsENOENT, and the whole invocation crashes before Secrets Manager is even reached. - When SNX does fetch the secret, it never writes it back to
/tmp, so every new container hits Secrets Manager again. This keeps the latency and the extra API charge that was supposed to be avoided.
Why this matters:
A cold-start crash means the keeper skips a cycle. Repeated Secrets Manager calls add 100ms+ to cold starts and burn through the free tier.
Recommendation
Wrap
fs.readFilein atry-catchthat suppresses only “file not found.” After fetching the secret, immediately write it to/tmpwith permissions0600so warm invocations can hit the cache. Maintain a module-level variable to prevent repeated file system access in the same invocation.Ensure the file written to
/tmpis encrypted. - The initial
-
M-03 Medium
KeeperWildcard SecretsManager Policy Access Control AcknowledgedDescription
A custom IAM role was introduced (good) but granted
secretsmanager:GetSecretValueon*. If an attacker gets Lambda code-exec they can read any secret in the account.Recommendation
Scope the policy to the two specific secret ARNs (private key and RPC URL) or use a dedicated Secrets Manager resource policy.
-
L-01 Low Interface Mismatch for
migrateLPSignature Logical Error AcknowledgedDescription
The
LPMigration.solcontract'smigratefunction was updated to address a previous vulnerability (High-01: Anyone can update users userLastDeposited). This remediation involved removing thereceiverparameter from themigratefunction, and instead, themsgSender(derived viaERC2771Context._msgSender()) is now implicitly used as the recipient of theAutoCompoundLPtokens.However, the
ILPMigration.solinterface still defines themigrateLPfunction with thereceiverparameter:function migrateLP(uint128 accountId, address receiver) external;This creates a discrepancy between the interface definition and the actual implementation in
LPMigration.sol.Recommendation
To maintain consistency and clarity, the
migrateLPfunction signature in theILPMigration.solinterface should be updated to match the implementedmigratefunction inLPMigration.sol.Also consider inheriting
ILPMigrationinLPMigration. -
L-02 Low L-16 Partially Fixed : Rounding Penalty Logical Error Acknowledged
Description
Division is still rounded down here
function decayingRedemptionPenalty( address user, uint256 assets ) public view returns (uint256) { ----------- return assets. mulDiv(timeAdjustedFeePercent, PERCENT_PRECISION); <<<<<<< }Recommendation
Consider rounding up for both of above divisions
-
L-03 Low Inconsistent Calculations in case of USDC depeg. Logical Error Acknowledged
Description
RewardsDistributionRegistry.getUsdValueOfRewards()returns the value of the available rewards inUSDC, notUSD(even though the name saysUsd). The v3 debt is being subtracted from the returned value. This will result in wrong calculations in case of a USDC depeg because the v3 debt is in USD while the returned value from the function is in USDC.Recommendation
Consider modifying
getUsdValueOfRewardsto return the value inUSD. If you do that, you should convert theabsAmountback to USDC inAutoCompoundLP.totalAssetsStata()before dividing it by the static tokenrate. -
L-04 Low Unfixed H-03 Warning Acknowledged
Description
The H-03 finding from the main review was not fixed because it's expected that rewards will always be distributed in
synths. However, given that the fix is easy and the impact of the issue would not be negligible if normal tokens were distributed as rewards, it's recommended to fix it, since the other part of code is already written to support this case.Recommendation
Consider implementing the fix from the original issue.
-
L-05 Low Alchemy API key can still be accessed Logical Error Acknowledged
Description
The
AlchemyAPI key was moved from theAutoCompoundLP.t.soland the other test files to an env, but it is still visible in the repository commit history.Recommendation
Consider deprecating this api key
-
L-06 Low ODOSRouter Max Approvals : Risk Increased Warning Acknowledged
Description
As mentioned in L-10, leaving approvals for the ODOS router poses security risks in case the router is exploited. The team acknowledged the issue, but after that the reward approvals to the ODOS router were changed to
uint256.max. This decision makes the situation riskier.Recommendation
Consider not using max approvals if they are not really needed.
-
L-07 Low
KeeperInvalidhandlersignature Logical Error AcknowledgedDescription
According to the AWS documentation, the
handler()function should accept parameters ofS3EventandContexttypes. This was true in the previous version of the code. However, with the latest changes, the handle function was modified to accept a boolean value, which is currently more suited for local dev than for prod use in AWS lambda environment.Recommendation
Check the documentation and revert back to the old signature in case it's needed.
-
L-08 Low
KeeperRPC-Key is still hardcoded Warning AcknowledgedDescription
In M-07, SNX said that
SECRET_ARNis just unique identifier used to reference private key, so they are ok with it, but theRPC URLshouldn't be publicly visible and "held separately ". However, the same url is still exposed in config.tsRecommendation
Ensure to implement the "To-Do", as stated in the comments, and avoid exposing the RPC URL. Keep in mind that it will still be visible in the commit history, hence its better to deprecate this key and use a fresh one.
-
L-09 Low
KeeperSlippage Tolerance Not Passed to ODOS in Quote Request Logical Error AcknowledgedDescription
As a fix for L-25, SNX has added slippage tolerance within its config. However, this value is not being passed when quoting via ODOS:
const requestBody: QuoteRequest = { chainId: 8453, compact: true, inputTokens: rewardTokens, outputTokens: [ { tokenAddress: USDC, proportion: 1, }, ], userAddr: slpAddr, };According to ODOS API documentation, slippage can be configured using the
slippageLimitPercentfield in the request body.Recommendation
Include the configured slippage tolerance by setting the
slippageLimitPercentfield in the request body. Note that ODOS supports only a single common slippage configuration for all tokens. (hence consider the most conservative one) -
L-10 Low
KeeperL21 Partiallly Fixed: Missing Error Handling forassembleOdosQuoteResponse Validation AcknowledgedDescription
As a fix for L-21, SNX has added a deprecation check. However, it still does not handle status codes returned from
assembleOdosQuote.Recommendation
Consider adding explicit error handling for the
assembleOdosQuotecall. -
L-11 Low
KeeperMissing Input Validation Validation AcknowledgedDescription
L-45 is marked as resolved, however proxy API still forwards raw requests and the Lambda does no schema checks. An internal caller sending malformed JSON could crash the function or cause expensive loops.
Recommendation
Add JSON schema validation (e.g. AJV) and early-return 400 on bad input to keep failures cheap.
-
L-12 Low
KeeperAll addresses aren’t checksummed Logical Error AcknowledgedDescription
ODOS requires all addresses to be checksummed according to their API documentation. However all the addresses aren’t checksummed.
There are several addresses, which are yet to be changed in
config.ts:cbBTC,cbETH,SNXRecommendation
Replace the non-checksum addresses with checksum addresses :
const SNX = '0x22e6966B799c4D5B13BE962E1D117b56327FDa66'; const cbETH = '0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22'; const cbBTC = '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf';
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 -
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.
