Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · February 2025

Protocol Review

for Vanir

Guardian's review of Protocol Review for Vanir, published February 2025. The report records 46 findings across 4 review rounds, including 1 critical and 6 high.

Published
Review window
November 28, 2024 to February 20, 2025
Rounds
Main Review, Remediation Review, Remediation Review 2, Remediation Review 3
Language
Solidity
Chains
Ethereum
Sector
Lending
  • 1 Critical
  • 6 High
  • 6 Medium
  • 33 Low
  • 0 Informational

27 resolved · 19 acknowledged

Scope

2 files in scope · 150 nSLOC
FilenSLOCLines
src/Vanir.sol124176
src/Interfaces.sol2628

Findings 46

Main Review

26 findings · November 28 to December 2, 2024
  1. H-01 High Liquidations/Withdrawals Might Revert If The User Can Not Receive Native Assets Logical Error Acknowledged
    Location
    Vanir.sol: 90
    Round
    Main Review

    Description

    The Vanir contract's liquidation and withdrawal functions may revert if the user cannot receive native Ether. In the withdrawEth and transferEth functions, the contract unwraps Wrapped Ether to Ether and attempts to send it to the user's address using payable(to).transfer(amount). This method assumes that the recipient address can accept Ether transfers. However, if the user's address is a smart contract without a payable fallback or receive function, or if it deliberately rejects Ether transfers, the transfer call will fail, causing the entire transaction to revert.

    Recommendation

    Consider creating a wrapper function to send Native assets. If the native transfer fails, consider wrapping the Ether into Wrapped Ether and transferring it to the destination address as an ERC20 token, which should never revert.

  2. H-02 High Liquidation Calls Can Be Sandwiched Logical Error Acknowledged
    Location
    Vanir.sol: 170
    Round
    Main Review

    Description

    The Vanir contract is susceptible to sandwich attacks during the liquidation process, specifically when swapping a portion of the collateral tokens for the borrowed tokens in the swap function. The liquidation process involves taking a flash loan of the borrowed token (e.g., DAI) to repay the user's debt. It then withdraws the user's collateral (e.g., WETH) by utilizing the user's approved spWETH tokens. The contract swaps a portion of this collateral to obtain the exact amount of debt tokens needed to repay the flash loan, using Uniswap's exactOutput function. This function allows the input amount (collateral tokens) to vary up to a maximum (amountInMaximum), which is the total collateral amount approved by the user. Because the transaction details will be visible in the public mempool, a malicious user could perform a sandwich attack by front-running and manipulating the token reserves unfavorably just before the swap occurs. This manipulation increases the swap's cost, causing the contract to consume more of the user's collateral than necessary. The attacker profits by selling tokens to inflate the price before the swap and buying them back at a lower price afterward. As a result, the user receives less collateral back after liquidation, leading to a loss beyond what is necessary to cover their debt.

    This issue can occur whenever the requests[i].collateral.amount value is higher than request.debtAmount as requests[i].collateral.amount will be used as the amountInMaximum parameter in the exactOutput call.

    Recommendation

    Whenever the liquidate function is called, consider setting requests[i].collateral.amount to a value equal to the expected amountOut value needed to repay the flashloan.

  3. H-03 High Liquidation Calls Can Be DoS’d DoS Resolved
    Location
    Vanir.sol: 166
    Round
    Main Review

    Description

    The swap function includes a require statement that compares the contract's balance of the collateral token (amountInMaximum) to the expected collateral amount to be liquidated (request.collateral.amount):

    require(amountInMaximum == request.collateral.amount, "vanir/contract balance not equal amount of collateral to be liquidated.");
    

    amountInMaximum is determined by the contract's current balance of the collateral token:

    uint amountInMaximum = Token(request.collateral.tokenAddress).balanceOf(address(this));
    

    An attacker can exploit this by sending a minimal amount (e.g., 1 wei) of the collateral token to the contract's address before a liquidation occurs. This token transfer increases the contract's balance, causing amountInMaximum to exceed request.collateral.amount. As a result, the require statement fails, and the liquidation transaction reverts. This vulnerability enables any malicious actor to indefinitely prevent liquidations by manipulating the contract's token balance.

    Recommendation

    Consider updating the require check from the swap function as shown below:

    require(amountInMaximum >= request.collateral.amount, "vanir/contract balance not equal amount of collateral to be liquidated.");
    
  4. H-04 High Liquidation Fee Is Wrongly Sent To The User Configuration Resolved
    Location
    Vanir.sol: 174
    Round
    Main Review

    Description

    In the Vanir contract, during the liquidation process, the liquidation fee specified by request.collateral.fee is incorrectly handled and ends up being returned to the user instead of being collected by the protocol or sent to a designated fee address. Specifically, in the swap function, after swapping the collateral tokens to repay the user's debt, the remaining collateral is calculated by subtracting only the swapCost from amountInMaximum :

    return (amountInMaximum - swapCost, swapCost);
    

    This calculation does not account for the liquidation fee (request.collateral.fee), meaning that the fee is not deducted from the user's remaining collateral. As a result, when the contract transfers the colAfterSwap amount back to the user, it includes the liquidation fee.

    Recommendation

    Update the calculation of the remaining collateral to ensure that the liquidation fee is properly deducted before returning funds to the user:

    amountAfterSwap = amountInMaximum - swapCost - request.collateral.fee;
    

    Additionally, implement logic to transfer the collected liquidation fee to a designated fee address.

  5. M-01 Medium Loan Function Does Not Support Multiple Requests With Native Ether Configuration Acknowledged
    Location
    Vanir.sol: 105
    Round
    Main Review

    Description

    The Vanir contract's loan function does not support multiple requests involving native Ether (ETH) within a single transaction. This limitation occurs because the function relies on msg.value to determine the amount of Ether supplied but msg.value represents the total amount of Ether sent with the transaction, not per individual request. In the loop within the loan function, when handling multiple LoanRequest items that require supplying Ether (isEth is true), each call to supplyEth expects msg.value to equal the amount specified for that request:

    require(msg.value > 0, "vanir/msg value must be larger than.");
    require(msg.value == amount, "vanir/msg value must be the same as amount");
    

    Since msg.value can not be different for each iteration, these require statements will fail if there are multiple Ether requests, as msg.value cannot simultaneously equal multiple different amount values. Even if the total msg.value covers all Ether amounts across requests, the per-request require checks will not pass. This means that users cannot perform multiple supply or withdrawal actions involving Ether in a single loan function call, limiting the contract's functionality and causing transactions to revert with an "out of funds" error.

    Recommendation

    To enable multiple requests involving native Ether within a single loan function call, the contract should be updated to handle Ether amounts per request rather than relying solely on msg.value. One possible approach is to accumulate the total Ether required for all LoanRequest items involving Ether and ensure that msg.value equals this total.

  6. M-02 Medium USDT Actions Are Blocked After First Withdrawal DoS Resolved
    Location
    Vanir.sol: 76
    Round
    Main Review

    Description

    USDT is not supported as collateral but is supported as an underlying asset in Spark. This means users can deposit USDT to earn interest. Unlike regular ERC-20 tokens, USDT token approvals work differently, and previous allowances must be cleared before granting new approvals.

    In the Vanir.withdraw function, an approval is granted to the lending pool, but this allowance is never utilized. After the first withdrawal of USDT through Vanir, all subsequent USDT deposits and withdrawals are blocked due to the lingering floating approval. Deposits fail when attempting to approve in the getTokensForPool function, and withdrawals fail when trying to grant new approval in the withdraw function.

    Recommendation

    Do not grant approvals when they are not needed. Alternatively, consider checking the token address and clearing previous approvals if the token is USDT.

  7. L-01 Low State Variables Missing Immutable Modifier Optimization Resolved
    Location
    Vanir.sol
    Round
    Main Review

    Description

    The Vanir contract declares several state variables that are assigned values only once during contract deployment and are not intended to change thereafter. Specifically, the variables flash, poolAddressProvider, and owner are set in the constructor and never modified again throughout the contract's lifecycle. However, these variables are not marked with the immutable keyword. In Solidity, variables that are assigned once during construction and remain constant can be declared as immutable, which offers gas savings by storing them directly in the bytecode rather than in storage. By not marking these variables as immutable, the contract incurs unnecessary gas costs for storage reads when accessing them.

    Recommendation

    Consider declaring the mentioned state variables as immutable.

  8. L-02 Low getUserAccountData Function Can Be Declared As View Optimization Resolved
    Location
    Vanir.sol: 52
    Round
    Main Review

    Description

    The getUserAccountData function in the Vanir contract is currently not declared with the view modifier, even though it does not modify any state variables. This function simply retrieves user account data from the lending pool by calling getPool().getUserAccountData(user) and returns the results. In Solidity, functions that do not alter the state of the contract should be marked as view. The absence of the view modifier leads to unnecessary gas costs as the EVM will treat the function as a state-changing transaction rather than a simple call.

    Recommendation

    Consider declaring the getUserAccountData as a view function.

  9. L-03 Low code Parameter Can Be Removed From The loan Function Optimization Resolved
    Location
    Vanir.sol: 105
    Round
    Main Review

    Description

    In the Vanir contract, the loan function includes a parameter code of type RequestCode, which is an enum. This parameter is not utilized within the function's internal logic to influence any operations or decisions. Its sole usage is in the emit Loan statement at the end of the function, where it is included in the emitted event:

    emit Loan(msg.sender, code, requests);
    

    Recommendation

    Consider removing the code parameter from both the loan function's parameters and the Loan event if it does not provide additional, necessary information that cannot be obtained from existing data like the requests array.

  10. L-04 Low USDS Liquidations Are Not Supported Optimization Resolved
    Location
    Vanir.sol: 124
    Round
    Main Review

    Description

    The Vanir contract's liquidation functionality currently does not support liquidations for USDS, even though USDS is one of the tokens that can be borrowed in the Spark protocol alongside DAI. This limitation is due to the contract's design, which specifically handles DAI liquidations and lacks the flexibility to process other debt tokens. In the liquidate function, the parameter debtTokenAddress is intended to represent the debt token to be repaid, but the subsequent logic in onFlashLoan and related functions assumes that this token is always DAI. For example, hard-coded addresses and assumptions about token behavior are tailored to DAI, making the liquidation process incompatible with USDS or any other debt token.

    Recommendation

    Consider refactoring the Vanir contract to support liquidations for any debt token supported by the Spark protocol.

  11. L-05 Low Liquidations Require A SPToken Approval To The Vanir Contract Logical Error Resolved
    Location
    Vanir.sol: 148
    Round
    Main Review

    Description

    The Vanir contract's liquidation process requires the user being liquidated to approve the contract to spend their spToken (Spark Protocol Token) collateral. Specifically, during liquidation, the contract calls the withdraw function, which attempts to transfer the user's spTokens to itself using getTokensForPool. This function checks the user's allowance and balance of spTokens and performs a transferFrom to move the tokens to the contract. However, this approach relies on the user's cooperation to grant approval to the Vanir contract for their spTokens. Without this approval, liquidations can not be performed.

    Recommendation

    Merely an informative issue. There is no way to perform this process without the prior user approval.

  12. L-06 Low Use Of Transfer Instead Of Call Configuration Acknowledged
    Location
    Vanir.sol: 90
    Round
    Main Review

    Description

    With the Solidity transfer() function, used to perform a transfer of a native asset, there is a gas limit of 2300 gas and the receiving smart contract should have a fallback function defined or else the transfer call will fail. This value is hardcoded to prevent reentrancy attacks.

    In the transferEth function this transfer call is used to transfer the native assets. Some multi-signature wallets require more than 2300 gas to receive a native asset transfer. For example, Gnosis Safe supports forwarding via fallback. Using transfer() with a Safe's multisig can lead to gas depletion and failed transfers. Some references can be found below:

    Recommendation

    It is recommended to use call over transfer to perform the transfer of a native asset. A proper gas limit should be set for the call making sure that is enough to cover transfers to any possible multi-signature wallets. Measures against reentrancy should be taken into consideration as well by for example using a nonReentrant lock.

  13. L-07 Low Owner State Variable Can Be Removed Optimization Resolved
    Location
    Vanir.sol: 9
    Round
    Main Review

    Description

    The Vanir contract declares an owner state variable that is initialized in the constructor to the address of the contract deployer (msg.sender). However, throughout the contract code, the owner variable is never used in any function or modifier to control access or execute owner-specific operations. There are no references to owner beyond its initialization, and it does not influence the contract's behavior in any way.

    Recommendation

    Consider removing the owner state variable from the Vanir contract.

  14. L-08 Low Insecure setAdmin Function Configuration Resolved
    Location
    Vanir.sol: 32
    Round
    Main Review

    Description

    The Vanir contract uses a setAdmin function to manage multiple admin addresses through an admins mapping. This admin role is critical because the admins are the only ones allowed to execute the liquidate function, which allows them to perform liquidations on user positions. If any admin account is compromised, they could abuse this privilege to drain user funds by providing harmful LiquidationRequest parameters or specifying a malicious swapRouterAddress.

    Recommendation

    Consider restricting the ability to add new admins exclusively to a single owner address. This owner should be managed by a multisig wallet. The setAdmin function should be updated so that only the owner can call it, preventing existing admins from granting admin rights to others. Finally, to implement secure ownership management, the contract should use OpenZeppelin's Ownable2Step contract, which implements a two-step ownership transfer pattern. This pattern requires the new owner to accept the ownership preventing accidental changes to the owner address.

  15. L-09 Low Unlicensed Smart Contract Configuration Acknowledged
    Location
    Vanir.sol
    Round
    Main Review

    Description

    The Vanir contract is currently marked as unlicensed, as indicated by the SPDX license identifier at the top of the file:

    // SPDX-License-Identifier: UNLICENSED
    

    Using unlicensed contracts can lead to legal uncertainties and conflicts regarding the usage, modification and distribution rights of the code.

    Recommendation

    It is recommended to choose and apply an appropriate open-source license to the smart contract. Some options are:

    1. MIT License: A permissive license that allows for reuse with minimal restrictions.
    2. GNU General Public License (GPL): A copyleft license that ensures derivative works are also open-source.
    3. Apache License 2.0: A permissive license that provides an express grant of patent rights from contributors to users.
  16. L-10 Low Decreased Health Factor After Liquidation Logical Error Resolved
    Location
    Vanir.sol: 152
    Round
    Main Review

    Description

    Liquidations on Vanir are performed to prevent liquidations on the Spark platform. During the liquidation process, a portion of the user’s collateral is withdrawn from Spark, swapped for the debt token to repay the flash loan, and any remaining collateral after the swap is refunded to the user. Ideally, a liquidation on Vanir should improve the user’s health factor on Spark.

    However, in the case of a partial liquidation, the user’s health factor will decrease if the value of the withdrawn collateral exceeds the repaid debt. This occurs because the excess collateral is not re-supplied to Spark but instead transferred to the user, effectively acting as a collateral withdrawal on behalf of the user.

    As a result, a liquidation on Vanir could potentially bring the user's position closer to liquidation on Spark, rather than preventing it.

    Recommendation

    Re-supply the excess collateral to Spark on behalf of the user in cases of partial debt repayment.

  17. L-11 Low Maximum Withdraw Is Not Supported Configuration Resolved
    Location
    Vanir.sol
    Round
    Main Review

    Description

    The Spark pool's withdraw() function allows the passed amount to be type(uint256).max. This will be treated as the whole available balance of the given user. Vanir doesn't support this feature because if the amount to be withdrawn is greater than the user's balance, the transaction will revert.

    Recommendation

    Document this limitation.

  18. L-12 Low Unused fee Parameter Optimization Resolved
    Location
    Vanir.sol
    Round
    Main Review

    Description

    A LoanRequests consists of Code and Asset. The Asset has a fee parameter passed by the user which is never used in Vanir.loan(). However, it will be emitted in the Loan event as part of the requests.

    Recommendation

    Consider using different Asset structs for loan requests and liquidation requests.

  19. L-13 Low Users Can Escape The Liquidation Fee Configuration Acknowledged
    Location
    Vanir.sol
    Round
    Main Review

    Description

    The idea of Vanir.liquidate() is to repay the debt of a given user by using their collateral before they get liquidated on Spark. By doing this a fee is to be charged by Vanir. However, users can just frontrun the liquidate transaction and repay their debt directly to avoid the liquidate() fee.

    Recommendation

    Be aware of this issue.

  20. L-14 Low Unnecessary Token Approval In withdraw Logical Error Resolved
    Location
    Vanir.sol: 76
    Round
    Main Review

    Description

    When withdrawing, the contract is burning aTokens (spTokens) to receive the underlying token. The lending pool doesn't need approval for the underlying token.

    The only approval needed is for the aTokens, which is already handled in getTokensForPool.

    Recommendation

    Remove the unnecessary approval.

  21. L-15 Low Unused fallback Function Optimization Resolved
    Location
    Vanir.sol: 29
    Round
    Main Review

    Description

    The contract is expected to receive ETH through the WETH contract, which triggers the receive function. Therefore, the fallback function is unused and can be removed.

    Recommendation

    Remove the fallback function.

  22. L-16 Low Referral Code Ommited Configuration Acknowledged
    Location
    Vanir.sol: 59, 69, 96, 100, & 126
    Round
    Main Review

    Description

    Calls to Spark harcode zero for the referral code. Although they do not currently have referrals set up, they mention in their documentation that they may add them in the future, Since the referral is hardcoded to zero, Vanir will not be eligible for the referral program in the future.

    Recommendation

    Use a variable, that can be updated in the future, to represent the referral code.

  23. L-17 Low setAdmin() Should Be external Optimization Resolved
    Location
    Vanir.sol: 32
    Round
    Main Review

    Description

    setAdmin() is marked as a public function. However, setAdmin() is not called at all within the smart contract. Therefore, it can be set to an external function, which is more gas efficient.

    Recommendation

    Change the function from public to external.

  24. L-18 Low Lack Of Reentrancy Guard Modifier Configuration Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    There are numerous external calls throughout the protocol that hand over the execution flow to smart contracts outside of the Vanir protocol. It is recommended to use a reentrancy guard to prevent any potential reentrancy attacks.

    Recommendation

    Use a reentrancy modifier on functions to prevent potential reentrancy attacks.

  25. L-19 Low Check Debt Amounts During liquidate Validation Resolved
    Location
    Vanir.sol: 124-127
    Round
    Main Review

    Description

    The liquidate function is called by the admins and used to liquidate some of the collateral of the user. The function accepts a LiquidationRequest[] requests array and every request has a debtAmount. The function also accepts another totalDebtAmount value, which will be flash-loaned.

    However, there is no validation to ensure that the sum of all requests.debtAmount values equals totalDebtAmount. If totalDebtAmount exceeds the sum of requests.debtAmount, the flash loan cannot be repaid. Conversely, in the opposite scenario, any excess funds swapped will be locked in the contract since there isn’t a rescue function.

    Recommendation

    Consider validating debt amounts when calling liquidate.

  26. L-20 Low Unvalidated Token Addresses Validation Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    During Vanir actions, token addresses are not validated. A user could set isEth to true while providing a token address other than WETH. Although such actions would be caught on the Spark side, it is best to validate token addresses within the Vanir contract, as it makes unsafe external calls to user-provided addresses.

    Recommendation

    Validate token addresses, particularly when isEth is true.

Remediation Review

8 findings · January 20, 2025
  1. C-01 Critical Unvalidated Parameters Allow an Attacker to Drain User Funds Validation Resolved
    Location
    Vanir.sol
    Round
    Remediation Review

    Description

    The executeOperation function permits callers to specify arbitrary parameters that will be used in different operations. This blind trust in user provided parameters allows the following exploit:

    • Alice approves the Vanir contract with 10 WETH.
    • Bob sees the approval and calls from a malicious contract address executeOperation(<WETH>, 10e18, 0, <Vanir address>, <params>) where params will be decoded to:
    address user = Alice
    address lendingPoolAddress = < Malicious address deployed by Bob >
    collateralAsset.amount = 10e18
    collateralAsset.token = WETH
    collateralAsset.data = data that will be decoded to
    	address spTokenAddress = WETH
    	uint feeAmount = 0
    	address swapRouterAddress = < Malicious address deployed by Bob >
    	address feeAddress = address(0)
    	bytes memory swapPath = ''
    

    The following code will be executed:

    // In executeOperation
    // Check below will pass because we will call this function from the lendingPoolAddress contract
    require(msg.sender == address(lendingPoolAddress), "vanir/flash loan not opened by lending pool.");
    // Initiator is controlled by Bob and was set to Vanir's address
    require(initiator == address(this), "vanir/flash loan must be initiated by Vanir.");
    // Will WETH approve Bob's malicious contract address
    Token(asset).approve(lendingPoolAddress, amount);
    // Call to Bob' malicious contract address. Repay mighy do nothing in Bob's malicious contract address
    ILendingPool(lendingPoolAddress).repay(asset, amount, 2, user); // only variable rate mode 2 supported
    // processLiquidation is called, lets continue the flow
    uint swapCost = processLiquidation(user, asset, amount, lendingPoolAddress, collateralAsset);
    
    // processLiquidation flow
    /*
        The parameters below will be decoded to as they are controlled by Bob:
    		address spTokenAddress = WETH
    		uint feeAmount = 0
    		address swapRouterAddress = < Malicious address deployed by Bob >
    		address feeAddress = address(0)
    		bytes memory swapPath = ''
    */
    (address spTokenAddress, uint feeAmount, address swapRouterAddress, address feeAddress, bytes memory swapPath) = abi.decode(collateralAsset.data, (address, uint, address, address, bytes));
    /*
    	 // Withdraw flow:
    	 // This will transfer the 10 WETH from Alice to Vanir address
       Token(spTokenAddress).transferFrom(from, address(this), amount);
       // The withdraw function will pull the 10 WETH from Vanir's address to the malicious contract address owned by Bob as it was previously approved
       ILendingPool(lendingPoolAddress).withdraw(tokenAddress, amount, to);
    */
    withdraw(user, address(this), collateralAsset.amount, collateralAsset.tokenAddress, spTokenAddress, lendingPoolAddress);
    
    // Bob already stole the WETH so the rest of the functions can simply be empty and return 0 values
    swapCost = swap(swapRouterAddress, collateralAsset.amount, flashLoanAmount + feeAmount, collateralAsset.tokenAddress, swapPath);
    if (feeAmount > 0) {
        Token(debtTokenAddress).transfer(feeAddress, feeAmount);
    }
    
    uint supplyAmount = collateralAsset.amount - swapCost;
    
    if (supplyAmount > 0) {
        Token(collateralAsset.tokenAddress).approve(lendingPoolAddress, supplyAmount);
        ILendingPool(lendingPoolAddress).supply(collateralAsset.tokenAddress, supplyAmount, user, 0);
    }
    
    return swapCost;
    
    // Back to executeOperation flow
    Token(asset).approve(lendingPoolAddress, amount + premium);
    emit Liquidation(user, collateralAsset, swapCost, asset, amount, premium);
    

    This way Bob managed to steal Vanir approved funds from Alice. Anytime a contract will call transferFrom(user, ...) based on an address passed in by an external call, you can end up pulling the user’s real tokens if they have an allowance. The contract must enforce that the lendingPoolAddress is truly whitelisted and not malicious.

    Recommendation

    Ensure that the lendingPoolAddress and the swapRouterAddress provided are whitelisted by the Vanir contract.

  2. H-01 High Unrestricted Direct Contract Interaction Validation Resolved
    Location
    Vanir.sol
    Round
    Remediation Review

    Description

    The Vanir contract is designed to work primarily via a centralized frontend. The expectation is that the backend logic will always supply “safe” parameters such as a legitimate lendingPoolAddress, swapRouterAddress, feeAddress, feeAmount, etc. However, there is no on-chain restriction preventing a user from calling the Vanir contract directly providing arbitrary parameters.

    Therefore, malicious users can pass a malicious contract as lendingPoolAddress or swapRouterAddress. The contract logic (executeOperation, processLiquidation, swap, etc.) will trust these addresses to perform critical actions like repaying debt, withdrawing collateral, and swapping tokens.

    Also, the contract allows specifying a custom feeAmount and feeAddress in actions like setup or interest. An attacker who directly calls the contract can simply set the feeAmount to zero to avoid paying any fee.

    Recommendation

    If the design truly requires a centralized backend to set those parameters, enforce a server-signed EIP-712 message containing the parameters. The smart contract would verify the signature before processing. This ensures malicious direct calls without a valid signature revert.

  3. H-02 High Liquidations Will Revert When Flashloan Premium Is Different Than 0 Logical Error Resolved
    Location
    Vanir.sol
    Round
    Remediation Review

    Description

    After a flashloan is initiated, AAVE performs a callback through the executeOperation function passing as parameters the loan amount and premium which are the fees to be paid.

    However, in Vanir's executeOperation, only amount is passed to processLiquidation. As a result, there won't be sufficient loan token swapped to repay the flashloan, and the entire liquidation process will fail.

    Recommendation

    Add premium to amount when calling the processLiquidation function:

    uint swapCost = processLiquidation(user, asset, amount + premium, lendingPoolAddress, collateralAsset);
    
  4. M-01 Medium USDT swap is blocked DoS Resolved
    Location
    Vanir.sol: 202
    Round
    Remediation Review

    Description

    In the swap() function Vanir approves the swapRouterAddress to spend amountIn tokens. It's expected that in most cases the router won't use up all of these tokens. Because the approval is not reset to 0, the difference between the originally approved amount and the actual spent amount will be left approved. This will cause DOS of the swap function for the USDT token which doesn't support changing allowances from a non-zero value to another non-zero value.

    Recommendation

    Consider clearing the approval to the swapRouterAddress after the swap concludes.

  5. L-01 Low Owner cannot be admin Code Best Practices Resolved
    Location
    Vanir.sol: 29
    Round
    Remediation Review

    Description

    The setAdmin() function doesn't allow the owner of the Vanir contract to be an owner as well. This can be inconvenient since you need 2 addresses for 1 admin.

    Recommendation

    You can change the require statement to:

    require(flag || msg.sender != admin, "vanir/owner cannot remove its own admin status.")

    This will allow the owner to add themselves as an admin, but not remove themselves.

  6. L-02 Low Typo Code Best Practices Resolved
    Location
    Vanir.sol: 29
    Round
    Remediation Review

    Description

    The error message inside Vanir.setAdmin() is "vanir/owner cannot change its own adminstatus.". There is no spacing between admin and status.

    Recommendation

    Add a spacing between the two words admin and status.

  7. L-03 Low Token And spToken Can Mismatch Validation Acknowledged
    Location
    Vanir.sol: 130-136
    Round
    Remediation Review

    Description

    During the withdrawal process, both tokenAddress and spTokenAddress are provided by the user. There is no check to ensure that these addresses match each other as their respective pairs on the underlying Spark protocol.

    Recommendation

    Check that tokenAddress and spTokenAddress are corresponding addresses.

  8. L-04 Low Validate Asset Is Not Used Twice Validation Acknowledged
    Location
    Vanir.sol: 110
    Round
    Remediation Review

    Description

    M-01 was acknowledged as intended behaviour, but the function loan() has no access restriction and can be called by any user. This can lead to the issue from M-01, where the function reverts with multiple interactions with ether in the same call.

    Recommendation

    Clearly document that loan() is intended to not handle the same asset twice in one function call. Additionally, consider adding validation to prevent users from calling loan() with multiple occurrences of the same asset.

Remediation Review 2

9 findings · February 8, 2025
  1. M-01 Medium User Can Pass Arbitrary Fee Amount Logical Error Acknowledged
    Location
    Vanir.sol
    Round
    Remediation Review 2

    Description

    Function borrow accepts an arbitrary feeAmount through a loan call. If Vanir expects a fee to be paid on borrow, a user can just directly interact with the contract and pass a feeAmount of zero to preserve as much capital.

    Recommendation

    Add validation on the expected feeAmount for Vanir. Alternatively, consider tieing feeAmount to a fixed percentage of the borrowed/liquidated amount rather than letting the caller supply an arbitrary fee, implement a fixed percentage model (e.g., feeAmount = (borrowedAmount * feePercentage) / 1e18).

  2. M-02 Medium Failed Approvals With USDT Logical Error Resolved
    Location
    Vanir.sol
    Round
    Remediation Review 2

    Description

    Function repay approves amount and then calls SparkLend to repay the amount. The issue is that the paybackAmount within the BorrowLogic is not necessarily equal to amount passed, so a portion of the approved amount will be not be utilized.

    This will lead to DoS with tokens such as USDT which require a 0 approval initially.

    Recommendation

    For all allowances in Vanir, use SafeERC20's forceApprove which will force the allowance to go to zero initially to handle tokens such as USDT. Also, use it after an external call to set the approval to zero after the approval is no longer necessary.

  3. M-03 Medium Lost borrow fee Validation Acknowledged
    Location
    Vanir.sol
    Round
    Remediation Review 2

    Description

    When users borrow via Vanir, a part of the borrowed assets is paid as a fee to the feeAddress, which is an arbitrary passed parameter, but is validated to be a whitelisted address.

    However, whitelisted addresses are also the swap routers and the lending pools. This means users may choose to pay the fee to any other whitelisted address resulting in loss of funds for Vanir.

    Recommendation

    Create a separate validation for the feeAddress.

  4. L-01 Low LendingPoolAddress Points To Registry Instead Deployment Resolved
    Location
    DeployVanir.s.sol
    Round
    Remediation Review 2

    Description

    The deployment script currently passes 0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE (the PoolAddressesProvider of Spark) to the Vanir constructor, instead of passing 0xC13e21B648A5Ee794902342038FF3aDAB66BE987 (the Spark LendingPool address on Ethereum mainnet). As a result, the Vanir contract is initialized with the wrong contract reference thereby preventing it from interacting correctly with the Spark lending protocol.

    If the contract is meant to integrate with Spark Lend on mainnet, the LendingPool address (0xC13e21B648A5Ee794902342038FF3aDAB66BE987) should replace the PoolAddressesProvider address in the script. Using the PoolAddressesProvider directly is incorrect in this context because Vanir specifically needs the active LendingPool to perform supply/borrow/repay operations, not just an address provider.

    Recommendation

    Update the constructor call in DeployVanir (or any relevant deployment script) to use 0xC13e21B648A5Ee794902342038FF3aDAB66BE987 instead of 0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE.

  5. L-02 Low No Controls On Liquidation Fee Amount Validation Resolved
    Location
    Vanir.sol
    Round
    Remediation Review 2

    Description

    During liquidation, the user pays an additional feeAmount from their collateral. This feeAmount is arbitrarily set by the admin within their LiquidationRequest, and can vary each time. Without fee validation, the fee may be too small or too large, negatively impacting the protocol and user respectively.

    Recommendation

    Consider adding on-chain validations for feeAmount on liquidations, or clearly document the fee calculation behavior of your backend system.

  6. L-03 Low Failed Liqudiations With Low Target LTV Warning Resolved
    Location
    Global
    Round
    Remediation Review 2

    Description

    Vanir has a target LTV for liquidation within its backend system. If the LTV which Vanir is targetting has a large delta between the current LTV, all liquidation calls will fail as more amountIn is necessary than is approved and available for swap.

    Recommendation

    Clearly document this behavior and appropriately set the target LTV.

  7. L-04 Low Failing Tests Warning Acknowledged
    Location
    Global
    Round
    Remediation Review 2

    Description

    The tests are meant to simulate Vanir's operations with current backend configurations, however numerous tests are failing. Some reasons for failure include but are not limited to:

    1. Lack of pinned fork block number
    2. Improper Target LTV's for partial liquidations
    3. Incorrect feeAmount calculation within the tests

    All tests should be passing prior to deployment.

    Recommendation

    Fix all tests and align them with Vanir's backend systems.

  8. L-05 Low No events emitted on funds rescue Event Acknowledged
    Location
    Vanir.sol
    Round
    Remediation Review 2

    Description

    Two new functions has been added to the contract to enable the admin to resuce funds - ‘transfer’ and ‘transferEth’. There are no events emitted to notify for the action happening.

    Recommendation

    Consider if you should emit events in these two functions.

  9. L-06 Low Frozen Pool Will Lead To Fail Liquidations Warning Acknowledged
    Location
    Global
    Round
    Remediation Review 2

    Description

    When the reserve configuration in SparkLend isFrozen, supplying liquidity is prevented by repays and withdraws are permitted. If the configuration were set to frozen, most Vanir liquidations would fail since function swap() attempts to supply liquidity that wasn't used as part of the swap cost.

    Recommendation

    Clearly document this risk.

Remediation Review 3

3 findings · February 20, 2025
  1. L-01 Low Incorrected Addresses Warning Acknowledged
    Location
    deploy-tenderly.json
    Round
    Remediation Review 3

    Description

    File deploy-tenderly.json has an incorrect lendingPool address (currently PoolAddressProvider) and feesAddress (currently the SparkLend Pool).

    Recommendation

    Update the addresses to 0xC13e21B648A5Ee794902342038FF3aDAB66BE987 and 0xc7453Ea3051eE01639eF13D67BF7d6a077ee77Dc respectively.

  2. L-02 Low Leftover SwapRouter Approval Warning Acknowledged
    Location
    Vanir.sol
    Round
    Remediation Review 3

    Description

    After function swap is performed, the SwapRouter may still have some token approval leftover if the entire amountIn was not utilized.

    Recommendation

    Remove the leftover approval after the swap.

  3. L-03 Low Overcharge in executeOperation Logical Error Acknowledged
    Location
    Vanir.sol
    Round
    Remediation Review 3

    Description

    As we know from the previous round, the SparkLend.repay() function may not use all of the tokens we pass as amount. In executeOperation the contract repays with the flashloaned tokens, withdraws collateral and swaps it for the flashloaned amount + premium. If the repay() function didn't use all of the flashloaned amount, the function will be overcharging the user.

    Recommendation

    Be aware and clearly document this behavior to users.

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote