Overview
This report covers the security review for SquadSwap protocol, a fork of PancakeSwap Infinity that adds more functionality, such as the Automated Liquidity Manager (ALM). This report includes the findings on the ALM. Our security assessment was a full review of the code, spanning a total of 1.5 weeks. During our review, we did not identif y any major severity vulnerability. We did identif y some minor severity vulnerabilities and code optimizations. All reported issues were fixed by the development team and subsequently verified by us. We can confidently say that the overall security and code quality have increased af ter completion of our audit.
Scope
The analyzed resources are located on:
The issues described in this report were fixed in the following commit:
Summary
Weaknesses
This section contains the list of discovered weaknesses.
SQUAD1-12 | ACTIVE ID SLIPPAGE CAN OMIT LIQUIDITY
Severity:
Status:
Fixed
Path:
contracts/Strategy.sol:enterPosition#L1093-L1312
Description:
The operator uses enterPosition to swap the strategy's assets if needed and add them as liquidity to SquadSwap bins. The function then records the minted ERC-1155 bin IDs and balances so that AUM calculations, user withdrawals, and operator exits can find the position later.
The recorded IDs should match the bins where liquidity was actually minted. This makes sure every asset removed from reserveX and reserveY remains included in getAUMWithFees() and can be recovered through the normal exit flow.
However, the strategy assumes every minted ID is activeIdDesired + deltaIds[i] and only checks balances at those IDs. SquadSwap instead mints at currentActiveId + deltaIds[i]; activeIdDesired is only used to check whether the current ID is within idSlippage. If the active ID moves inside that allowed range, the position is minted into different bins than the strategy records.
The assets used by SquadSwap are still removed from the strategy reserves, while the actual ERC-1155 balances are left out of tokenBalances. With zero or missing per-bin minimums, the call does not revert. The omitted liquidity is then excluded from AUM and cannot be burned by withdrawals, exitPosition, or partialExitPosition. A partial mismatch can also understate the share price and give later depositors too many shares.
For example:
- The operator enters a one-bin position with
activeIdDesired = D,deltaIds = [0],idSlippage = 1, and a zero minimum. - A public trade or the strategy's own swap moves the pool's active ID to
D + 1before liquidity is added. - SquadSwap accepts the move and mints the strategy's position at
D + 1. - The strategy checks only bin D, records a zero balance, and still subtracts the deployed assets from its reserves.
- AUM now omits the position, and
exitPositionreverts withNoPosition. The liquidity stays stranded until the operator deliberately rediscovers the real bin in a later entry.
// Full source code excerpt omitted from the public version to protect proprietary implementation details
Remediation:
Record and validate the exact bin IDs minted by the position manager, and require binIds, tokenBalances, and minLiquidities to have matching lengths. If the integration cannot return the minted IDs, require idSlippage to be zero so the expected and actual IDs cannot differ.
SQUAD1-13 | DEPOSITS CAN FORCE LIQUIDITY INTO IDLE
Severity:
Status:
Fixed
Path:
contracts/Strategy.sol:withdrawWithFees#L442-L621
Description:
The withdrawWithFees function lets shareholders redeem their shares for a proportional part of the strategy's reserves and LP position. Public strategy users can reach it after _depositInternal prices their deposit against the full AUM and leaves the deposited tokens in reserveX and reserveY.
When the reserves cannot fully cover a redemption, the function should remove only enough liquidity to cover the actual shortfall. A small change in a tracked bin should therefore not materially change the operator's LP allocation.
However, the function uses an all-or-nothing reserve check. If either reserve is short by even a negligible amount, it switches to the liquidity branch and burns the redeemer's full pro-rata share of every tracked bin:
// Full source code excerpt omitted from the public version to protect proprietary implementation details
An attacker can use this to replace a large part of the managed LP position with idle reserves:
- The attacker deposits both assets in the strategy's AUM ratio and receives a large share of the supply. The assets stay in the reserves.
- The attacker makes a small swap or one-sided donation through a tracked bin, increasing the strategy's claim to one token.
- The attacker immediately calls
withdrawWithFees. The changed claim makes one reserve slightly too small, so the function burns the attacker's full ownership fraction from every bin. - The attacker receives their fair pro-rata assets, but the same fraction of the previous LP position is left as idle reserves for the remaining holders. Any residual bin balance also prevents
enterPositionfrom redeploying those reserves until the operator fully exits the position.
// Full source code excerpt omitted from the public version to protect proprietary implementation details
Remediation:
Remove only enough liquidity to cover the reserve shortfall on each token instead of burning the redeemer's full pro-rata share of every bin. Also clear zero or dust bin entries so the operator can redeploy the remaining reserves directly.
SQUAD1-14 | SHIFTED COMPACT BIN IDS CAUSE INCORRECT POSITION-LIMIT QUOTES
Severity:
Status:
Fixed
Path:
contracts/StrategyLens.sol:quoteCounterTokenAmount#L129-L183
Description:
The operator calls quoteCounterTokenAmount to work out how much of the counter token should be supplied when a strategy has a position limit. The function loads the strategy's canonical value distribution from StrategyManager, matches it to the submitted execution bins, and calculates the token ratio. The resulting quote is then passed to Strategy.enterPosition as counterAmountMax.
When zero-weight bins are removed, each remaining execution bin should keep the weight of its original canonical bin. This must also hold when the target price shifts the execution bin IDs relative to the pool's current active ID.
However, _getCompactedValueDistribution treats each shifted execution ID as if it were still a canonical ID:
// Full source code excerpt omitted from the public version to protect proprietary implementation details
This selects the wrong weights after a price shift and may also map past the end of the array, causing a revert. For example, [0, 25, 25, 25, 25, 0] shifted by one bin compacts to execution IDs [-1, 0, 1, 2], all with weight 25. The lens instead loads [25, 25, 25, 0]. With token0 as the primary token, this quotes a counter-token ratio of 1.00 instead of 0.60, a 66.7% overquote.
An example flow is:
- A strategy uses a position limit and a distribution containing zero-weight bins.
- The target price differs from the pool's active ID, so the execution IDs are shifted, and the zero-weight bins are removed.
- The operator calls
quoteCounterTokenAmountwith the compact execution IDs. - The lens loads weights using the shifted IDs and returns an incorrect quote or reverts.
- If the operator uses the quote,
enterPositioneither leaves usable funds idle or supplies more counter tokens than the configured shape calls for. A revert blocks the normal re-entry or rebalance flow.
// Full source code excerpt omitted from the public version to protect proprietary implementation details
Remediation:
Keep each bin's canonical index when compacting and pass those indices, or the already-compacted weights, to the lens. Do not derive canonical distribution indices from shifted execution IDs.