Saddle Finance

QuantstampFiles named in the report

Quantstamp reviewed Saddle (Saddle Finance) on 3 Nov 2020: 19 findings, 0 critical; 12 marked fixed, 7 acknowledged.

No open critical findings
Open the reportOpen Saddle’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings19

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium022
Low059
HighIncorrect computation in `getD`Fixed

The `SwapUtils.getD` function contains an incorrect `add(1)` for the denominator of `dP` inside the inner `for`-loop, on L241. This will lead to an incorrect value of `D` being returned every time `SwapUtils.getD` is called.

MediumInteger Overflow / UnderflowFixed

Integer overflow/underflow occur when an integer hits its bit-size limit. Every integer has a set range; when that range is passed, the value loops back around. A clock is a good analogy: at 11:59, the minute hand goes to 0, not 60, because 59 is the largest possible minute. Integer overflow and underflow may cause many unexpected kinds of behavior and was the core reason for the `batchOverflow` attack. <br> The subtraction inside `SwapUtils.withdrawAdminFees` does not use `SafeMath`, which could cause an underflow if `token.balanceOf(address(this)) < self.balances[i]`. The result of the underflow would then be assigned to the `uint256 balance` variable, which would be positive and therefore always pass the check on the subsequent line: `if (balance > 0)`. <br> A similar underflow issue occurs on L504 inside `SwapUtils.getAdminBalance`.

MediumMissing input validationFixed

The following functions are missing checks for user input: 1. **[Fixed]** The `Swap.getToken` function does not check if the value of the `index` parameter is lower than the length of the `pooledTokens` array. 2. **[Fixed]** The `Swap.getTokenBalance` function does not check if the value of the `index` parameter is lower than the length of the `pooledTokens` array. 3. **[Fixed]** The `Swap.calculateSwap` function does not check if value of the 2 `tokenIndex{From | To}` parameters are lower than the length of the `pooledTokens` array. It also does not check if the value of the `dx` input parameter is lower than the amount of tokens that the user actually has. 4. **[Fixed]** The `Swap.calculateRemoveLiquidity` function does not check if value of the `amount` parameter is larger than the total supply of the `swapStorage.lpToken`. 5. **[Fixed]** The `Swap.calculateRemoveLiquidityOneToken` function does not check if the value of the `tokenIndex` parameter is lower than the length of the `pooledTokens` array. It also doesn't check if the value of the `tokenAmount` is lower than the actual amount available for that token. 6. **[Fixed]** The `Swap.getAdminBalance` function does not check if the value of the `index` parameter is lower than the length of the `pooledTokens` array. 7. **[Fixed]** The `Swap.swap` function does not check if value of the 2 `tokenIndex{From | To}` parameters are lower than the length of the `pooledTokens` array. It also does not check if the value of the `dx` input parameter is lower than the amount of tokens that the user actually has. 8. **[Fixed]** There should be checks for `poolAddress != address(0x0)` in the functions `Allowlist.setPoolAccountLimit`, and `Allowlist.setPoolCap`. 9. **[Fixed]** The `Swap.constructor` does not check if the length of the `_pooledTokens.length > 1`. This is an implicit assumption, which should be made explicit. 10. **[Fixed]** The `Swap.constructor` does not check if all the addresses provided in `_pooledTokens` are different. This would allow pools with multiple tokens, where the tokens have the same address. The consequences of an exploit of the aforementioned items vary. However, some of the items mentioned above could significantly impact the reputation of the project and should be therefore addressed.

Low`stopRampA` may be called repeatedly even after a ramp has endedFixed

There is no verification when the `stopRampA()` function is called to check if: 1. the current ramp has already ended due to the `self.futureATime` being in the past. 2. the `stopRampA()` was already called before to stop the current ramp. This allows calling this function multiple times (consecutively) with the effect that each time the `self.initialATime` is set to the current block timestamp, which prevents a new ramp for `MIN_RAMP_TIME` seconds. This could be problematic if the `stopRampA` function is called multiple times by mistake.

LowChecks-effects-interactions pattern violated in `addLiquidity`Fixed

The [checks-effects-interactions pattern](https://solidity.readthedocs.io/en/develop/security-considerations.html?highlight=check%20effects#use-the-checks-effects-interactions-pattern) is respected by most methods with one notable exception being the `SwapUtils.addLiquidity` method.

LowCrude check of contract address does not workFixed

The return value of the following call on L94: `allowlist.getAllowedAmount(address(this), address(0)); // crude check of the allowlist`, is ignored inside the `Swap.constructor`. It seems that this function is called to check if the `allowlist` address is indeed the address of an `Allowlist` contract instance. However, there are 2 problems with this approach: 1. The address can point to any address even `address(0)`, an EOA or a contract that does not have the `getAllowedAmount` and that call will still not cause a revert and will only return `0`. 2. If the address indeed points to the `Allowlist` contract, then the parameters passed to this call: `allowlist.getAllowedAmount(address(this), address(0));` should correctly return `0`. Therefore, wrapping the call to `allowlist.getAllowedAmount` in a `require` statement that checks if the return value of that function is equal to `0` will not confirm if the address indeed points to the `Allowlist` contract.

LowInteger OverflowFixed

The `SwapUtils.rampA()` function uses primitive addition (+) and multiplication (*) operators on L921, L922 and L925. The latter could cause an overflow if the value of the `futureA_` input parameter is too large.

LowMethods with different names and same implementationFixed

The 2 functions `SwapUtils.calculateRemoveLiquidity` and `SwapUtils.calculateRebalanceAmounts` have the same implementation logic. However, they have different names and the former is `external` and the latter is `internal`.

LowMissing assertion in `removeLiquidityImbalance`Fixed

The [reference implementation](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/base/SwapTemplateBase.vy#L577) of the `removeLiquidityImbalance` function contains an additional assertion that check if the `tokenAmount` is different from zero, before adding one to it. This is missing on L759 in Saddle.

LowRamping can be started while previous ramp is still ongoingFixed

The error message of the first `require` statement inside `rampA()` says that: "Ramp already ongoing". However, this `require` statement only checks if the `MIN_RAMP_TIME` has passed since the last call to `rampA()`, when the value of `self.initialATime` was set. The time when the previous ramp ends is actually given by `self.futureATime`, which is required to be greater or equal to `self.initialATime + MIN_RAMP_TIME` on L922.

Show 2 more
LowUnlocked PragmaFixed

Every Solidity file specifies in the header a version number of the format `pragma solidity (^)0.5.*`. The caret (`^`) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version _and above_, hence the term "unlocked".

LowVirtual price calculation is not future-proofFixed

The `SwapUtils.getVirtualPrice` claims to calculate "the virtual price, scaled to the POOL_PRECISION". In order to do this it multiplies `D` by `10 ** uint256(getPoolPrecisionDecimals())` and divides the result by `self.lpToken.totalSupply()`. Even though in the current implementation inside `Swap.sol` we can see that the `LPToken` is created with the value of `SwapUtils.getPoolPrecisionDecimals()` as the number of decimals, this may change during maintenance or when adding features and there is no mechanism inside `SwapUtils.getVirtualPrice` that will indicate that the number of decimals of the `self.lpToken` is no longer equal to `getPoolPrecisionDecimals()`. This would lead to an incorrect virtual price.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (16)

  • saddle/saddle-contract-master/contracts/Allowlist.sol
  • saddle/saddle-contract-master/contracts/Swap.sol
  • saddle/saddle-contract/contracts/OwnerPausable.sol
  • saddle/saddle-contract-master/contracts/LPToken.sol
  • saddle/saddle-contract/contracts/StakeableTokenWrapper.sol
  • saddle/saddle-contract/contracts/CERC20.sol
  • saddle/saddle-contract/contracts/Swap.sol
  • saddle/saddle-contract/contracts/MathUtils.sol
  • saddle/saddle-contract-master/contracts/OwnerPausable.sol
  • saddle/saddle-contract/contracts/SwapUtils.sol
  • saddle/saddle-contract/contracts/Allowlist.sol
  • saddle/saddle-contract-master/contracts/MathUtils.sol
  • saddle/saddle-contract-master/contracts/CERC20.sol
  • saddle/saddle-contract-master/contracts/StakeableTokenWrapper.sol
  • saddle/saddle-contract/contracts/LPToken.sol
  • saddle/saddle-contract-master/contracts/SwapUtils.sol

Part of Saddle’s security record

Saddle
Audits4 audits by 2 firmsLast audit Sep 2021, by Quantstamp.
Critical findingsNo open critical findingsAcross 4 reports.
IncidentIncident 29 Apr 2022Source: De.Fi incident database.
Open Saddle’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us