As of 30 Oct 2026, Solv Protocol has 6 audit reports by 3 security firms on Trustblock; the latest is by Quantstamp, dated 15 Dec 2025. Those reports list no open critical findings. It also holds 2 deployed contracts on 1 chain.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Solv Protocol - BTC Redemption | Security firm: Quantstamp | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · certificate.quantstamp.com |
| Audit Report (SolvBTC) | Security firm: Paladin | Date: | Scope: 2 addresses on Ethereum | No open critical findings | Open · IPFS |
| Solv Protocol - SolvBTC | Security firm: Quantstamp | Date: | Scope: Repository at commit eafc741 | No open critical findings | Open · certificate.quantstamp.com |
| Solv Yield - Bearing Tokens | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Solv Protocol- Vault Guardian | Security firm: Quantstamp | Date: | Scope: Repository at commit 80300ac | No open critical findings | Open · certificate.quantstamp.com |
| Solv Protocol stUSD - Audit | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 2 | 1 | 4 |
| Medium | 3 | 0 | 3 |
| Low | 7 | 10 | 25 |
The Solv Vault Guardian is designed to regulate multi-signature operations through a guard mechanism within the Safe. It utilizes the `checkTransaction()` function to validate the legitimacy of operations against configured rules for each `BaseAuthorization`. An operation proceeds if it meets at least one rule. The implementation allows for independent checks across multiple authorization contracts, with the process halting upon the first successful authorization. This design can inadvertently facilitate the execution of operations, such as token transfers and approvals if just one among potentially multiple authorization checks is lenient or explicitly permits the action.
Open the reportThe current test suite for the Solv Vault Guardian lacks comprehensive coverage potentially leaving the protocol vulnerable to untested edge cases and integration issues. Specifically, the testsuite does not fully address protocol compatibility, function selector accuracy, and various transaction scenarios, including those involving native currency and tokens across different protocols such as Agni, GMXV1, GMXV2, and Lendle. Identified Gaps: 1. **Lack of Protocol Compatibility Tests**: Lacks tests verifying Agni, GMXV1, GMX2, Lendle, and Cobo Argus protocol integrations. 2. **Lack of Safe Multisend Compatibility Tests**: Lack of tests verifying the guardian's validation of safe multisend functions. 3. **Safe Upgrade to 1.4**: No tests covering the upgrade of a Safe from version 1.3 to 1.4, missing checks for compatibility or security issues. 4. **Gnosis Safe Deployment in Tests**: The test suite relies on a deployed Safe instance, requiring access to an owner's private key, which complicates and potentially compromises testing integrity.
Open the reportUpon deployment, both `GMXV1Authorization` and `GMXV2Authorization` add support for the ERC20 transfer and approve functions on a select number of whitelisted tokens. As a result, the Safe can freely invoke any transfer or approve operations on tokens held by the Safe, and bypass any of the necessary logic included in the `ERC20ApproveAuthorization` or `ERC20TransferAuthorization` contracts. Further, only the `GMXV1Authorization` would need to be enabled for there to be transfers, or approvals, of any whitelisted tokens from the SafeAccount. The overlap present can lead to an inconsistent state of approval, due to the fact that one authorizer would reject the transaction, whilst the other would allow it.
Open the reportUpon detailed review of the `burn()` function, it is discovered that it lacks proper validation checks for scenarios where the input `sftId_` corresponds to the `holdingValueSftId` managed by the contract itself. In such cases, users engage the `burn()` function, expecting to exchange their ERC-20 tokens for an equivalent amount of ERC-3525 tokens. However, due to the absence of a check verifying that `sftId_` is distinct from `holdingSftId` and ensuring the caller is indeed the owner of `sftId_`, the ERC-20 tokens are burned without the corresponding release of ERC-3525 tokens to the user. This constitutes a critical flaw as it leads to a unilateral loss for the user without the anticipated token exchange.
Open the reportThe `swtAddress_` parameter of `unstake()` function is missing a check to ensure it is deployed by the `SftWrappedTokenFactory`, allowing users to specify an arbitrary address as the `swtAddress_`. Calling a user provided address is not safe, especially in a public function with no access control restriction. For example, if a non-standard ERC3525 token is transferred to the router without triggering `onERC3525Received` or `onERC721Received`, a malicious `swtAddress_` contract could be used to transfer the token to the attacker (L149).
Open the reportBoth `ERC20ApproveAuthroization` and `ERC3525ApproveAuthorization` are intended to enforce a whitelist of approved spenders for tokens held by the Safe. However, the lack of verification on the function selector of the transaction being invoked creates the possibility for an arbitrary transaction to be approved. As there is no ACL contract associated with these Authorizations, there is no `preCheck()` call that would fail in the case where a function is trying to be invoked that is not defined by the ACL. As a result, arbitrary functions can be more readily enabled and executed, as they only have to be added as an allowed selector to the Authorization. The exploitability of this issue is significantly limited by the fact that the Governor address would have to be compromised to support a malicious action, as well as the fact that for the malicious action to be invoked in the first place, there would still have to be the quorum of required signatures met on the Safe. Regardless, the possiblity of such should not be included in the Guard. The following exploit scenario considers the situation where the Governor is trying to transfer all USDT from the Safe, as we think this example is easy to follow. However, it could also work with any function on any contract that the compromised Governor account chooses to add. Although, the function called would still have to have arguments that can be decoded to `(address, uint256)`.
Open the reportThis [EIP](https://eips.ethereum.org/EIPS/eip-3525#erc-3525-token-receiver) defines a simple ‘Check, Notify and Response’ model for better flexibility as well as simplicity: 1. No extra `safeTransferFrom` methods are needed, all callers only need to call one kind of transfer; 2. All ERC-3525 contracts MUST check for the existence of `onERC3525Received` on the recipient contract and call the function when it exists; 3. Any smart contract can implement `onERC3525Received` function for the purpose of being notified after receiving values; this function MUST return 0x009ce20b (i.e. `bytes4(keccak256('onERC3525Received(address,uint256,uint256,uint256,bytes)'))`) if the transfer is accepted, or any other value if the transfer is rejected. There is a special case for this notification/acceptance mechanism: since ERC-3525 allows value transfer from an address to itself, when a smart contract which implements `onERC3525Received` transfers value to itself, `onERC3525Received` will also be called. This allows for the contract to implement different rules of acceptance between self-value-transfer and receiving value from other addresses. However, this contract does not follow the above rule and does not implement the `onERC3525Received()`.
Open the reportWhen `acceptAdmin()` is called, the `pendingAdmin` variable is deleted without emitting an event that reflects its change to the zero address. While a `NewAdmin` event is emitted for the admin change, the lack of an explicit event for the `pendingAdmin` update can lead to inconsistencies for off-chain indexers or monitoring systems that rely on events to track the lifecycle of `pendingAdmin`.
Open the reportThe `withdrawFeeRate` parameter has no explicit upper bound enforced at the contract level. Although the calculation includes a safety check: `require(withdrawFee <= solvBTCAmount_)` this still allows configurations where the effective fee can approach 100% of the redemption amount, effectively resulting in near-zero `solvBTC` output for users, minimizing withdrawals amount for all users.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe `admin` of the `SftWrappedTokenFactory` contract can upgrade the implementation contract for a given `productType_` by calling `setImplementation()` followed by a call to `upgradeBeacon()`. However, if `upgradeBeacon()` is not called, the `productTypes` mapping in the `SftWrappedTokenFactory` contract will have a mismatched `implementation` address than what is set in the beacon contract.
Open the reportWhen the contract admin attempts to remove an SFT slot from the `SolvBTCMultiAssetPool`, the pool contract must have a zero balance of the given SFT slot before removal. Therefore, if even one user fails to withdraw their SFT, the admin will be unable to remove the slot, which allows other users to deposit the SFT slot continually into the contract.
Open the reportWhen executing `stake()` in the `SftWrapRouter` contract, the SFT wrapped token registered for the SFT slot in the `SftWrappedTokenFactory` contract is obtained via a call to `SftWrappedTokenFactory.sftWrappedTokens()`. If the returned `sftWrappedToken` address is the zero address, execution will fail, and staking cannot occur for the given SFT slot. This check prevents users from staking SFT slots that are only registered in the `SolvBTVMultiAssetPool` and not registered in `SftWrappedTokenFactory`. Further, the function `_stakeSft()`, which is executed after the `SftWrapRouter` contract receives the SFT, only deposits into the legacy `SftWrappedToken` contract if the `SolvBTCMultiAssetPool` contract disallows the SFT slot. Therefore, the zero address check in `stake()` will prevent non-legacy-supported SFT slots from being staked.
Open the reportThe `createSubscription()` is a payable function, indicating it allows users to transfer native tokens to the router. Line 164 of the contract calls the function `ERC20TransferHelper.doTransferIn()` to transfer native tokens to the router if `poolInfo.currency` being equal to `ETH_ADDRESS`. However, the router contact does not deposit the received native tokens to the designated contract `IOpenFundMarket(openFundMarket)` (L167), and the `IOpenFundMarket::subscribe()` is not a payable function.
Open the reportContract `SftWrapRouter` extends `ReentrancyGuardUpgradeable`, but the extended contract is not initialized by the current contract. Generally, the initializer function of a contract should always call all the initializer functions of the contracts that it extends.
Open the reportThe `stake()` function ensures the token amount to be wrapped cannot be zero: ```solidity=117 require(amount_ > 0, "SftWrapRouter: stake amount cannot be 0"); ``` Users can transfer tokens directly to the router without triggering the `stake()` function, thereby bypassing the zero-amount check.
Open the reportTest code should be removed prior to on-chain deployment: ```solidity=15 import "../lib/forge-std/src/console. Sol"; ```
Open the reportThe `SftWrapRouter` contract does not protect their initializers. An attacker can call the initializer and assume ownership of the logic contract, whereby she can perform privileged operations that trick unsuspecting users into believing that she is the owner of the upgradeable contract.
Open the report"Dead" code refers to code which is never executed and hence makes no impact on the final result of running a program. Dead code raises a concern, since either the code is unnecessary or the necessary code's results were ignored. There is dead code in the following places: 1. `ETH` in `ERC20ApproveAuthorization` 2. `ETH` in `ERC20TransferAuthorization` 3. `GLP_REWAED_ROUTER` in `GMXV1AuthorizationACL` 4. `repayWithBalance()` in `SolvOpenEndFundAuthorizationACL`
Open the reportCurrently, the `SolvOpenEndFundAuthorization` does not initialize with support for the `repayWithBalance()` function despite that function being included in the `SolvOpenEndFundAuthorizationACL`.
Open the reportThe owner of the contracts can call `transferGovernance()` 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 `onlyGovernor()` modifier can no longer be executed.
Open the reportThe idea behind ACL contracts is that they are used to check selector call values via the ACL contracts. This is done via a self static call with the expected selector signature. The ACL contracts implement the allowed functions in the contract. It is expected that only those functions should be exposed, whilst in the following contracts, these are also public functions: 1. `checkToken()` in `LendleAuthorizationACL` 2. `checkToken()` in `AgniAuthorizationACL`
Open the reportThe `addAuthorizations()` function allows the governor to add authorizers of type `BaseAuthorization`. It is assumed that all `authorizations[i].executor` have an implementation of the `authorizationCheckTransaction()` function. This is important because if the function is not implemented, it could lead to unexpected approvals or denials.
Open the reportAs it presently is constructed, the contract is vulnerable to the [allowance double-spend exploit](https://github.com/comitylabs/openzeppelin-contracts/blob/6bd6b76d1156e20e45d1016f355d154141c7e5b9/contracts/token/ERC20/IERC20.sol#L43), as with other ERC20 tokens.
Open the report`GMX_REWAED_ROUTER_V2` and `GMX_REWAED_ROUTER` in`GMXV1Authorization`.
Open the reportOne of the core designs of the protocol is the iteration of `BaseAuthorization`, which checks if the transaction should be allowed or not. Each loop iteration calls `executor.authorizationCheckTransaction()`, which in itself does its own checks and calls to ACL check contracts. This system works if there are not many `BaseAuthorization`. Otherwise, the gas requirement would scale very fast. This could lead to transactions being rejected or normal transactions costing significant gas amounts due to `checkTransaction()` being invoked on all execution calls from the Safe Vault.
Open the reportThe `_allowedContractToFunctions` stores a mapping of allowed contracts and their corresponding allowed function selectors, while the `_contractACL` mapping stores target contract addresses and corresponding ACL contract addresses. One can add an address to `_contractACL` mapping via `setContractACL()`, but the function does not check if that target contract address is already in `_allowedContractToFunctions`, which is essential, as unless the contract is in that mapping, the flow will never reach the `_contractACL`.
Open the reportEvery Solidity file specifies in the header a version number of the format `pragma solidity (^)0.8.*`. 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 reportAddresses are not validated before assignment or external calls, potentially allowing the use of zero addresses and leading to unexpected behavior or vulnerabilities. For example, transferring tokens to a zero address can result in a permanent loss of those tokens. ```solidity=61 wrappedSftAddress = wrappedSftAddress_; ``` - `wrappedSftAddress_` is not zero-checked before being used. --- ```solidity=63 navOracle = navOracle_; ``` - `navOracle_` is not zero-checked before being used. --- ```solidity=30 pendingAdmin = newPendingAdmin_; ``` - `newPendingAdmin_` is not zero-checked before being used. --- ```solidity=30 pendingGovernor = newPendingGovernor_; ``` - `newPendingGovernor_` is not zero-checked before being used.
Open the reportPerforming division by zero would raise an error and revert the transaction. ```solidity=132 return value * (10 ** decimals()) / latestNav; ``` The expression `value * (10 ** decimals()) / latestNav` may divide by zero. Its divisor has has estimated interval [0, 115792089237316195423570985008687907853269984665640564039457584007913129639935].
Open the report| Address | Chain | Source code |
|---|---|---|
| 0x2265…8a680x2265a8e449ea5ff78ee4c115370e9b5957358a68 | Chain: Ethereum | Source code: Verified source |
| 0xb129…6e8e0xb12979ff302ac903849948037a51792cf7186e8e | Chain: Ethereum | Source code: Verified source |
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.
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.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="solv-protocol"></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