Quantstamp reviewed Solv Protocol (Solv Protocol- Vault Guardian) on 25 Mar 2024: 20 findings, 0 critical; 14 marked fixed, 5 acknowledged, 1 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 | 3 |
| Medium | 1 | 0 | 1 |
| Low | 0 | 5 | 10 |
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.
The 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.
Upon 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.
Both `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)`.
"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`
Currently, the `SolvOpenEndFundAuthorization` does not initialize with support for the `repayWithBalance()` function despite that function being included in the `SolvOpenEndFundAuthorizationACL`.
The 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.
The 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`
The `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.
As 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.
`GMX_REWAED_ROUTER_V2` and `GMX_REWAED_ROUTER` in`GMXV1Authorization`.
One 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.
The `_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`.
Every 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".
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (36)
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