Quantstamp reviewed Secured Finance on 12 Dec 2023: 45 findings, 0 critical; 23 marked fixed, 17 acknowledged, 5 open.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 4 |
| Medium | 1 | 3 | 6 |
| Low | 4 | 14 | 13 |
Administrators of the Secured Finance protocol can modify a set of parameters that can lead to liquidations and market instability: 1. `TokenVaultStorage.slot().collateralCurrencies` set is queried when calculating the available collateral of users. This set can be modified by the owner of `TokenVault`, calling `TokenVault.updateCurrency()`. If this administrator removes a currency from the set, this currency will not be used in the collateral calculation, and it will be worth zero (in user positions and deposited funds), potentially leading to a cascade of user liquidations. 2. Updating haircut values can uncover the positions of some users. 3. If the contract `ReserveFund` is not paused and the liquidated user is insolvent, `ReserveFund` can cover in some conditions a portion of the amount transferred to the liquidator. If replicable, this process could lead to draining funds from the contract `ReserveFund`. This is expected to be protected by the Circuit Breaker and minimum debt price modules, and rapid liquidation executions. Protocol operators should be careful when updating the threshold and minimum debt price values. Still, adding more tests of that specific process would help to make sure it is not possible to execute this scenario, by considering several edge cases and extreme conditions.
In case of an emergency, a termination mechanism can be executed by the owner of the contract `LendingMarketController` to cease the operations of the protocol. Once executed, this operation cannot be reverted. For each active currency, the last aggregated price returned by the oracle is stored in the mapping `marketTerminationPrices`. Then, users can force a settlement of all their lending and borrowing positions via the function `executeEmergencySettlement()`. However, if the emergency is caused by incorrect prices returned by the oracle, these prices will be recorded as reference prices and it would negatively impact the settlement phase as the coverage calculation would provide incorrect values.
The modifier `onlyAcceptedContracts()` is used by several contracts to restrict some of their operations to a list of addresses identified by their associated name registered in the mapping of the contract `AddressResolver`. This mapping can be updated by the owner of this contract. As a result, if the owner's address is compromised or if it becomes malicious, an authorized address corresponding to an accepted contract could be replaced by a malicious address which would get the right to execute restricted operations. The likelihood is low but the negative impact for the protocol would be high.
The `OrderBookLogic` contract contains two functions that invoke the `initialize()` function on orderbook instances, `createOrderBook()`, which is called when the protocol owner attempts to create a new orderbook in an existing market, and `executeAutoRoll()`, which auto-rolls the current maturity to the next one. When the owner calls `createOrderBook()`, they specify a maturity, an opening date, and a pre-opening date, and the initialization function sets those values to the passed maturity's entry in the `isReady()` mapping. After a while, when `executeAutoRoll()` is called by `LendingMarketOperationLogic.rotateOrderBooks()`, the following parameters are passed to it, among others: - `_newMaturity`, which is the result of `calculateNextMaturity()`. This will be used as the key to the `isReady` mapping. - `_openingDate`, which is the maturity date of the newly opened orderbook. This will be set as the next orderbook's opening date. The function also initializes the pre-opening date of the orderbook to `_openingDate - OrderBookLib.PRE_ORDER_BASE_PERIOD`. As a result, the opening date and pre-opening date are recalculated for every orderbook, which renders the initial setup useless.
The owner of the contracts inheriting from OpenZeppelin's `Ownable` can call `transferOwnership()` to transfer the ownership to a new address. If an uncontrollable address is accidentally provided as the new owner address then the contract will no longer have an active owner, and functions with the `onlyOwner` modifier can no longer be executed. The following critical roles are not transferred following a two-step pattern: * the role `proxyAdmin` in the function `ProxyController.changeProxyAdmins()`; * the role `owner` in the contracts inheriting the contract `Ownable`; If the role is transferred to an address owned by no one, the role can be considered to be lost.
The `remove()` function performs a `delete self.nodes[cursor]` call towards its execution's end. The `nodes` mapping maps `uint256` values to `Node` struct values, which in turn contain a mapping `orders` that keeps track of orders under such values. Deletion in this fashion will result in the `Node` struct's instance having its values reset to their defaults, except for the `orders` mapping which will keep its contents intact. This may result in unwanted situations where a node is removed and its orders remain.
The `removeCurrency()` function only removes the supplied `_ccy` value from the `currencies` mapping, however the `haircuts`, `priceFeeds`, and `decimalsCaches` entries for the given currency are not cleared.
The following functions are called but the calling functions ignore the values returned without explicit justification: 1. `LendingMarketController.cleanUpFunds()` in `TokenVault._withdraw()`; 2. `GenesisValueVault.executeForcedReset()` in `FundManagementLogic._resetFundsPerCurrency()`; 3. `LiquidationLogic._transferCollateral()` in `LiquidationLogic.executeLiquidation()`; 4. `LiquidationLogic._transferCollateral()` in `LiquidationLogic.executeForcedRepayment()`; 5. `LiquidationLogic._transferPositionsPerMaturity()` in `LiquidationLogic.executeLiquidation()`; 6. `TokenVault.transferFrom()` in `LiquidationLogic.executeForcedRepayment()`; If not intended, it can result in unexpected behavior of the system. If intended, it makes it harder for an external observer to understand why returned values are ignored.
In `Contracts.sol`, as the names of the contracts are used as `bytes32` keys in a mapping, using a string with a representation exceeding `bytes32` could break the assumption that the same key only refers to one contract name.
If the owner renounces their ownership, all ownable contracts will be left without an owner. Consequently, any function guarded by the `onlyOwner` modifier will no longer be able to be executed.
The `executeLiquidation()` function performs a heavy logic to liquidate a position. The function calls other lengthy and gas-intensive functions such as `FundManagementLogic.cleanUpFunds()` and `TokenVault.getLiquidationAmount()`, which may result in failed liquidations due to insufficient gas usage.
The `executeLiquidation()` and `executeForcedRepayment()` functions both perform external function calls on user-supplied addresses. This allows for the possibility of reentrancy vulnerabilities.
These are the privileged operations identified during the audit: <br> #### AddressResolver - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Update the mapping between contract names and the address of the associated contract used by the protocol <br> #### BeaconProxyController - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Update the implementation contract to an arbitrary contract for the contracts: - LendingMarket - FutureValueVault - AcceptedContracts can: - Deploy FutureValueVault contracts. - Deploy LendingMarket contracts. <br> #### CurrencyController - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Add a new currency (or override the parameters of an existing one) with the following parameters: number of decimals, haircut value, list of price feeds and heartbeat value. - Remove a currency - Update the haircut value of an existing currency - Update or remove the price feeds list of an existing currency <br> #### FutureValueVault - Accepted Contracts can: - Increase the balance of a user - Decrease the balance of a user - Transfer tokens from a user - Reset all amount of a user if there is an amount in a past maturity - Set the initial total supply - Force the reset of the future value of a user <br> #### GenesisValueVault - Accepted Contracts can: - Initialize currency settings - Trigger the update of the initial compound factor - Execute an AutoRoll - Update genesis value with future value - Update genesis value with residual amount - Transfer tokens from a user - Cleanup the balance of a user per maturity - Force the reset of the genesis value of a user <br> #### LendingMarket - Accepted Contracts can: - Create an order book - Execute an autoRoll - Cancel an order - Execute an order - Execute a preorder - Unwind a position - Execute the itayose call - Update the order fee rates - Update the Circuit breaker limit range - Pause the contract - Unpause the contract <br> #### LendingMarketController - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Update the order fee rates of a given currency - Update the Circuit breaker limit range of a given currency - Initialize a lending market - Create an order book - Trigger an emergency termination to stop the protocol - Update the minimum debt unit price - Operator can: - Pause a lending market - Unpause a lending market - Renounce its role - DefaultAdmin can: - Grant DefaultAdmin role to an address - Revoke DefaultAdmin role from an address - Renounce its role - Grant Operator role to an address - Revoke Operator role from an address - Update the admin role of a role, which means which roles can manage (grant/revoke) other roles. <br> #### ProxyController - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Update in a batch the admin of a set of proxies deployed by this contract to an arbitrary new admin. - Update the implementation contract to an arbitrary contract and provide initialization data for the contracts: - AddressResolver - BeaconProxyController - TokenVault - CurrencyController - GenesisValueVault - LendingMarketController - ReserveFund - For each contract: - a UpgradeableProxy is deployed if none was deployed until now for this given contract. - If the new contract contains: - a self.destruct instruction, existing proxy contracts could self-destruct. - a function to transfer all funds to an arbitrary address, existing proxy contracts could be drained. <br> #### ReserveFund - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Execute a transaction (or a batch) via arbitrary call to arbitrary addresses with arbitrary payloads. It includes sending to an arbitrary address tokens owned by the `ReserveFund` contract. - Deposit tokens to the token vault - Withdraw tokens held by ReserveFund from the token vault <br> #### TokenVault - Owner can: - Renounce ownership - Transfer ownership (maybe to uncontrolled address) - Register a currency to be accepted as a collateral - Unregister a currency to be accepted as a collateral - Accepted Contracts can: - Trigger a deposit of collateral from any user that approved that contract to do so - Add a deposit amount for a user - Remove a deposit amount for a user - Force the reset of the deposit amount of a user - Transfer tokens from a user - Operator can: - Pause the token vault - Unpause the token vault - Renounce its role - DefaultAdmin can: - Grant DefaultAdmin role to
Some ERC20 tokens can change the balance of accounts outside of transfer methods. These tokens are commonly referred to as rebasing tokens. Since the protocol only ensures that an expected amount of tokens is received after a transfer, tokens that change the balance through other methods will disrupt internal accounting. Here are some tokens that can also affect the protocol operation: - Tokens that can be paused - Tokens with allowlist/blocklist - Tokens that can be upgraded
The price feeds associated with a currency `C` can temporarily not update their price. In this case, this will be detected via the function `CurrencyController._getAggregatedLastPrice()` which will raise an error `StalePriceFeed`. This would make all flows involving getting a price for `C` revert. However, any other flow will still work, including the ones involving getting prices of other currencies.
The following functions let a transaction execute normally even if it does not result in a state modification, resulting in gas spent when it was not needed: 1. In the function `ProxyController.changeProxyAdmins()` if the list `destinations` is empty 2. In the functions `ReserveFund.pause()` and `ReserveFund.unpause()` 3. In the function `CurrencyController.removeCurrency()` if the removed currency does not exist
Most of the contracts within the Secured Finance protocol are upgradeable, implementing the EIP-1967 storage slots in transparent and beacon proxy patterns. While this is not a vulnerability, users should be aware that the behavior of the protocol could drastically change if the contracts are upgraded. **Furthermore, new vulnerabilities not present during the audit could be introduced in upgraded versions of the contract, including the transfer of funds or transfer tokens that were allowed to the protocol.** This audit does not guarantee the behavior of future contracts that may be upgraded.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (70)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething 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