Saddle security record

Arbitrum, Ethereum, Fantom, Optimismsaddle.financeXGitHub

As of 3 Nov 2026, Saddle has 4 audit reports by 2 security firms on Trustblock; the latest is by Quantstamp, dated 24 Sep 2021. Those reports list no open critical findings. It also holds 2 incidents (latest 29 Apr 2022).

Critical findings
No open critical findings
Across 4 reports.
Incident
Incident 29 Apr 2022
Source: De.Fi incident database. No audit on Trustblock is dated after it. See the incidents
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.

Audits4

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Saddle TokenSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
Saddle Finance VirtualSwapSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
Saddle Contracts AuditSecurity firm: OpenZeppelinDate: Scope: Files named in the reportNo open critical findingsOpen · blog.openzeppelin.com
Saddle FinanceSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com

Findings48

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

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium126
Low3629
HighIncorrect computation in `getD`Saddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
MediumMismatch between inline specification and `withdraw` functionSaddle Finance VirtualSwap · Quantstamp · 7 Mar 2021Fixed

The comment on L275-276 states: "Reverts if the given `itemId` does not represent a `synthToToken` or a `tokenToSynth` swap." However, the function checks `swapType > PendingSwapType.TokenToSynth`, which allows either `SynthToToken` or `TokenToToken` swap types.

Open the report
Medium[M01] Pools may have duplicate tokensSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Medium[M02] Initial fee rates and amplification coefficient can be set out of boundsSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Medium[M04] Token fees can lead to incorrect calculationsSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
MediumInteger Overflow / UnderflowSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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`.

Open the report
MediumMissing input validationSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
LowConstant May Violate RequirementsSaddle Token · Quantstamp · 24 Sep 2021Fixed

The constant `DURATION = 2 * (52 weeks)` is not documented. Since the provided requirement states that "transfer is blocked until 52 weeks pass...", it is not clear if this is defined correctly.

Open the report
LowGas Concerns With Constructor and VestingSaddle Token · Quantstamp · 24 Sep 2021Fixed

Given that the constructor is using a loop to distribute the initial amount of the token inside `Vesting()` contracts, there is the concern that given an enough long list of recipients the constructor will run out of gas. Also, there is no other way to include further recipients afterward if the list of recipients list has to be shortened.

Open the report
LowInteger overflowSaddle Token · Quantstamp · 24 Sep 2021Fixed

Since [Solidity 0.8.x does not perform safe casting by default](https://github.com/ethereum/solidity/issues/10195), there is a potential integer overflow on L96 in `RetroactiveVesting.sol` where a summation of 2 `uint256` variables is cast to `uint120`.

Open the report
Show 10 more
LowMissing Check for Zero ETH TransfersSaddle Token · Quantstamp · 24 Sep 2021Fixed

On `rescueTokens()` there is a check to avoid transferring 0 tokens, but there is a check missing to transfer 0 ETH.

Open the report
LowUnchecked Function ArgumentsSaddle Token · Quantstamp · 24 Sep 2021Fixed

1. `Vesting.initialize` should ensure that `_token` is non-zero and `_durationInSeconds` is non-zero (the latter is to avoid a possible divide-by-zero in `vestedAmount`). 2. `RetroactiveVesting.constructor` should ensure that all three address arguments are non-zero. 3. `SDL.constructor` should check that `_pausePeriod > 0` and also `_pausePeriod <= 52 weeks`. If the value were set to zero, it would allow immediate transfers since `_pause` would not be invoked. The constructor should also check that `_governance`, `_vestingContractTarget`, and each subfield of `_recipients` is non-zero (except `cliffPeriod`). 1. `RetroactiveVesting.verifyAndClaimReward()` does not check if `totalAmount` is greater than zero and if it fits into `uint120`. Due to the cast on L69, this could lead to an integer overflow in case `totalAmount` is greater than the largest integer in `uint120`. 3. `Vesting.changeBeneficiary()` does not check if `newBeneficiary != beneficiary`. 5. `SimpleGovernance.changeGovernance()` does not check that `newGovernance != governance`.

Open the report
LowUnclear Logic for Paused TransfersSaddle Token · Quantstamp · 24 Sep 2021Fixed

According to *natspec* the addresses in `allowedTransferee` are allowed to transfer tokens even if the contract is paused. Is understood that these addresses can be `from` addresses when transferring. But in the logic at `_beforeTokenTransfer()` if the `to` address is in allowed then the transfer is allowed (`allowedTransferee[from] || allowedTransferee[to]`). This logic seems unclear and could allow users to partner with an allowed accomplice account to drain funds out of the contract, even during a paused contract scenario.

Open the report
LowUnclear Separation Of DutiesSaddle Token · Quantstamp · 24 Sep 2021Fixed

The `GenericERC20WithGovernance` contract extends `SimpleGovernance` and its description in the code comments indicates: "This contract simulates a generic ERC20 token that is mintable and burnable." First, it is important to note that there is no publicly accessible burn function. Therefore, there is a deviation from the specification at this point. Second, and more importantly, the `owner` and not the `governance` address is the one which is only allowed to call the `mint()` function. This is confusing as one would expect the decision to mint tokens would be given to the `governance` address, not to the `owner`.

Open the report
LowUnclear Vesting Policy When Beneficiary Is ChangedSaddle Token · Quantstamp · 24 Sep 2021Fixed

The function `changeBeneficiary` allows governance to set a new beneficiary address for any `Vesting` contract. Suppose the `Vesting` contract owns 100 total tokens and they are 50% vested. If the old beneficiary address has recently been claimed, they would have received up to 50 tokens and the new beneficiary would only receive around 50 as well (after full vesting). However, if the previous beneficiary has not claimed any tokens yet, that address will not be entitled to any tokens after the change, and therefore the new address will be allotted all 100 tokens after the full vesting period. It is not clear which case is desirable.

Open the report
LowUnclear Vesting Revocation PolicySaddle Token · Quantstamp · 24 Sep 2021Fixed

The contract mentions that vesting is "optionally revocable by the owner" and later mentions that `currentBalance` can be 0 in case of vesting being revoked earlier." However, it does not appear that any revocation logic exists.

Open the report
LowVesting Release May Be Blocked By Integer UnderflowSaddle Token · Quantstamp · 24 Sep 2021Fixed

The `Vesting.vestedAmount()` function returns the minimum between `currentBalance` and `unreleased` on L129, and the comment right above on L128 indicates: "`currentBalance` can be 0 in case of vesting being revoked earlier." However, in the case when `currentBalance == 0`, the subtraction on L126 would underflow and the function would throw. This would also seemingly block the beneficiary from calling `release()`. However, since the intended vested amount would be anyway `0` it would effectively not make a difference to the beneficiary's balance. Nevertheless, the intended error message: "No tokens to release" would not be displayed to the beneficiary.

Open the report
Low`completeToSynth` is callable by any userSaddle Finance VirtualSwap · Quantstamp · 7 Mar 2021Fixed

The function `completeToSynth` is callable by any user after the `maxSecsLeftInWaitingPeriod`. Although the `synth` wil be sent to the correct `nftOwner` regardless of `msg.sender`, this could have impact on external contracts that rely upon calling `completeToSynth`. For example, suppose some contract has a function of the form: ``` function getSynthAndTransfer(...) { Bridge.completeToSynth(id); synth.transfer(external_addr); } ``` If another arbitrary user invokes `completeToSynth` before the contract invokes it, the funds could be locked forever (assuming the contract did not have sufficient "retrieval" functions).

Open the report
LowSettle cloned swappers can be destroyedSaddle Finance VirtualSwap · Quantstamp · 7 Mar 2021Fixed

To save some gas on settlement and storage in general (in case of backup or migration), a cloned `SynthSwapper` that has already been used for settled swaps, can be destroyed. You can also remove the swaps from the pending list.

Open the report
LowStates `ReadyToSettle`, `PartiallyCompleted` and `Completed` are never checked and can lead to abnormal calls.Saddle Finance VirtualSwap · Quantstamp · 7 Mar 2021Fixed

These pending swap states are set but never checked anywhere; this must be due to implicit assumptions on the state of swaps, but opens the door for duplicated calls to, for example, `completeToToken()`. A second duplicated call to `completeToToken()` might be possible with `swapAmount = 0` even if it was completed before.

Open the report
Show 10 more
LowUnchecked function return valuesSaddle Finance VirtualSwap · Quantstamp · 7 Mar 2021Fixed

In order to ensure that the functions updated state properly, it should be checked that `transfer` calls in `swapSynthToToken` and `withdraw` return true.

Open the report
Low[L02] Low test coverage on some contractsSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Low[L03] Improper math correction in _updateUserWithdrawFeeSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Low[L04] No return values for some functionsSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Low[L05] Outdated Solidity version in useSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Low[L06] Token precisions not verifiedSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Low[L07] Loss of accuracy caused when dividingSaddle Contracts Audit · OpenZeppelin · 11 Dec 2020Fixed

The report gives no description here.

Open the report
Low`stopRampA` may be called repeatedly even after a ramp has endedSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
LowChecks-effects-interactions pattern violated in `addLiquidity`Saddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
LowCrude check of contract address does not workSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
Show 6 more
LowInteger OverflowSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
LowMethods with different names and same implementationSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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`.

Open the report
LowMissing assertion in `removeLiquidityImbalance`Saddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
LowRamping can be started while previous ramp is still ongoingSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report
LowUnlocked PragmaSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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".

Open the report
LowVirtual price calculation is not future-proofSaddle Finance · Quantstamp · 3 Nov 2020Fixed

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.

Open the report

Incidents2

Attached through a shared contract address. Our incident data ends in Oct 2026.

Incident

Incident,

Source: De.Fi incident database.

Audits before it
4.
Audits since
None on Trustblock.
Incident

Incident,

After the Saddle Finance official launch, some pools were arbitraged for millions within hours of launch.

Source: De.Fi incident database.

Audits before it
2.
Audits since
2, the latest dated 24 Sep 2021.

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="saddle-finance"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

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