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
Scope
2 files in scope · 150 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/Vanir.sol | 124 | 176 |
src/Interfaces.sol | 26 | 28 |
Findings 46
Main Review
26 findings · November 28 to December 2, 2024-
H-01 High Liquidations/Withdrawals Might Revert If The User Can Not Receive Native Assets Logical Error Acknowledged
Description
The
Vanircontract's liquidation and withdrawal functions may revert if the user cannot receive native Ether. In thewithdrawEthandtransferEthfunctions, the contract unwraps Wrapped Ether to Ether and attempts to send it to the user's address usingpayable(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.
-
H-02 High Liquidation Calls Can Be Sandwiched Logical Error Acknowledged
Description
The
Vanircontract is susceptible to sandwich attacks during the liquidation process, specifically when swapping a portion of the collateral tokens for the borrowed tokens in theswapfunction. 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 approvedspWETHtokens. The contract swaps a portion of this collateral to obtain the exact amount of debt tokens needed to repay the flash loan, using Uniswap'sexactOutputfunction. 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.amountvalue is higher thanrequest.debtAmountasrequests[i].collateral.amountwill be used as theamountInMaximumparameter in theexactOutputcall.Recommendation
Whenever the
liquidatefunction is called, consider settingrequests[i].collateral.amountto a value equal to the expectedamountOutvalue needed to repay the flashloan. -
H-03 High Liquidation Calls Can Be DoS’d DoS Resolved
Description
The
swapfunction includes arequirestatement 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.");amountInMaximumis 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
amountInMaximumto exceedrequest.collateral.amount. As a result, therequirestatement 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
swapfunction as shown below:require(amountInMaximum >= request.collateral.amount, "vanir/contract balance not equal amount of collateral to be liquidated."); -
H-04 High Liquidation Fee Is Wrongly Sent To The User Configuration Resolved
Description
In the
Vanircontract, during the liquidation process, the liquidation fee specified byrequest.collateral.feeis 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 theswapfunction, after swapping the collateral tokens to repay the user's debt, the remaining collateral is calculated by subtracting only theswapCostfromamountInMaximum: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 thecolAfterSwapamount 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.
-
M-01 Medium Loan Function Does Not Support Multiple Requests With Native Ether Configuration Acknowledged
Description
The
Vanircontract'sloanfunction does not support multiple requests involving native Ether (ETH) within a single transaction. This limitation occurs because the function relies onmsg.valueto determine the amount of Ether supplied butmsg.valuerepresents the total amount of Ether sent with the transaction, not per individual request. In the loop within theloanfunction, when handling multipleLoanRequestitems that require supplying Ether (isEthistrue), each call tosupplyEthexpectsmsg.valueto equal theamountspecified 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.valuecan not be different for each iteration, theserequirestatements will fail if there are multiple Ether requests, asmsg.valuecannot simultaneously equal multiple differentamountvalues. Even if the totalmsg.valuecovers all Ether amounts across requests, the per-requestrequirechecks will not pass. This means that users cannot perform multiple supply or withdrawal actions involving Ether in a singleloanfunction 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
loanfunction call, the contract should be updated to handle Ether amounts per request rather than relying solely onmsg.value. One possible approach is to accumulate the total Ether required for allLoanRequestitems involving Ether and ensure thatmsg.valueequals this total. -
M-02 Medium USDT Actions Are Blocked After First Withdrawal DoS Resolved
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.withdrawfunction, 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 thegetTokensForPoolfunction, and withdrawals fail when trying to grant new approval in thewithdrawfunction.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.
-
L-01 Low State Variables Missing Immutable Modifier Optimization Resolved
Description
The
Vanircontract declares several state variables that are assigned values only once during contract deployment and are not intended to change thereafter. Specifically, the variablesflash,poolAddressProvider, andownerare set in the constructor and never modified again throughout the contract's lifecycle. However, these variables are not marked with theimmutablekeyword. In Solidity, variables that are assigned once during construction and remain constant can be declared asimmutable, which offers gas savings by storing them directly in the bytecode rather than in storage. By not marking these variables asimmutable, the contract incurs unnecessary gas costs for storage reads when accessing them.Recommendation
Consider declaring the mentioned state variables as
immutable. -
L-02 Low getUserAccountData Function Can Be Declared As View Optimization Resolved
Description
The
getUserAccountDatafunction in theVanircontract is currently not declared with theviewmodifier, even though it does not modify any state variables. This function simply retrieves user account data from the lending pool by callinggetPool().getUserAccountData(user)and returns the results. In Solidity, functions that do not alter the state of the contract should be marked asview. The absence of theviewmodifier 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
getUserAccountDataas aviewfunction. -
L-03 Low code Parameter Can Be Removed From The loan Function Optimization Resolved
Description
In the
Vanircontract, theloanfunction includes a parametercodeof typeRequestCode, 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 theemit Loanstatement at the end of the function, where it is included in the emitted event:emit Loan(msg.sender, code, requests);Recommendation
Consider removing the
codeparameter from both theloanfunction's parameters and theLoanevent if it does not provide additional, necessary information that cannot be obtained from existing data like therequestsarray. -
L-04 Low USDS Liquidations Are Not Supported Optimization Resolved
Description
The
Vanircontract'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 theliquidatefunction, the parameterdebtTokenAddressis intended to represent the debt token to be repaid, but the subsequent logic inonFlashLoanand 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
Vanircontract to support liquidations for any debt token supported by the Spark protocol. -
L-05 Low Liquidations Require A SPToken Approval To The Vanir Contract Logical Error Resolved
Description
The
Vanircontract's liquidation process requires the user being liquidated to approve the contract to spend theirspToken(Spark Protocol Token) collateral. Specifically, during liquidation, the contract calls thewithdrawfunction, which attempts to transfer the user'sspTokensto itself usinggetTokensForPool. This function checks the user's allowance and balance ofspTokensand performs atransferFromto move the tokens to the contract. However, this approach relies on the user's cooperation to grant approval to theVanircontract for theirspTokens. 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.
-
L-06 Low Use Of Transfer Instead Of Call Configuration Acknowledged
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
transferEthfunction thistransfercall 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. Usingtransfer()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
callovertransferto perform the transfer of a native asset. A proper gas limit should be set for thecallmaking 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 anonReentrantlock. -
L-07 Low Owner State Variable Can Be Removed Optimization Resolved
Description
The
Vanircontract declares anownerstate variable that is initialized in the constructor to the address of the contract deployer (msg.sender). However, throughout the contract code, theownervariable is never used in any function or modifier to control access or execute owner-specific operations. There are no references toownerbeyond its initialization, and it does not influence the contract's behavior in any way.Recommendation
Consider removing the
ownerstate variable from theVanircontract. -
L-08 Low Insecure setAdmin Function Configuration Resolved
Description
The
Vanircontract uses asetAdminfunction to manage multiple admin addresses through anadminsmapping. This admin role is critical because the admins are the only ones allowed to execute theliquidatefunction, 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 harmfulLiquidationRequestparameters or specifying a maliciousswapRouterAddress.Recommendation
Consider restricting the ability to add new admins exclusively to a single
owneraddress. Thisownershould be managed by a multisig wallet. ThesetAdminfunction should be updated so that only theownercan call it, preventing existing admins from granting admin rights to others. Finally, to implement secure ownership management, the contract should use OpenZeppelin'sOwnable2Stepcontract, which implements a two-step ownership transfer pattern. This pattern requires the new owner to accept the ownership preventing accidental changes to theowneraddress. -
L-09 Low Unlicensed Smart Contract Configuration Acknowledged
Description
The
Vanircontract is currently marked as unlicensed, as indicated by the SPDX license identifier at the top of the file:// SPDX-License-Identifier: UNLICENSEDUsing 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:
- MIT License: A permissive license that allows for reuse with minimal restrictions.
- GNU General Public License (GPL): A copyleft license that ensures derivative works are also open-source.
- Apache License 2.0: A permissive license that provides an express grant of patent rights from contributors to users.
-
L-10 Low Decreased Health Factor After Liquidation Logical Error Resolved
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.
-
L-11 Low Maximum Withdraw Is Not Supported Configuration Resolved
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.
-
L-12 Low Unused
feeParameter Optimization ResolvedDescription
A
LoanRequestsconsists ofCodeandAsset. TheAssethas afeeparameter passed by the user which is never used inVanir.loan(). However, it will be emitted in theLoanevent as part of therequests.Recommendation
Consider using different
Assetstructs for loan requests and liquidation requests. -
L-13 Low Users Can Escape The Liquidation Fee Configuration Acknowledged
Description
The idea of
Vanir.liquidate()is to repay the debt of a given user by using their collateral before they get liquidated onSpark. By doing this a fee is to be charged by Vanir. However, users can just frontrun theliquidatetransaction and repay their debt directly to avoid theliquidate()fee.Recommendation
Be aware of this issue.
-
L-14 Low Unnecessary Token Approval In withdraw Logical Error Resolved
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.
-
L-15 Low Unused fallback Function Optimization Resolved
Description
The contract is expected to receive ETH through the WETH contract, which triggers the
receivefunction. Therefore, thefallbackfunction is unused and can be removed.Recommendation
Remove the
fallbackfunction. -
L-16 Low Referral Code Ommited Configuration Acknowledged
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.
-
L-17 Low
setAdmin()Should BeexternalOptimization ResolvedDescription
setAdmin()is marked as apublicfunction. However,setAdmin()is not called at all within the smart contract. Therefore, it can be set to anexternalfunction, which is more gas efficient.Recommendation
Change the function from
publictoexternal. -
L-18 Low Lack Of Reentrancy Guard Modifier Configuration Acknowledged
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.
-
L-19 Low Check Debt Amounts During
liquidateValidation ResolvedDescription
The
liquidatefunction is called by the admins and used to liquidate some of the collateral of the user. The function accepts aLiquidationRequest[] requestsarray and every request has adebtAmount. The function also accepts anothertotalDebtAmountvalue, which will be flash-loaned.However, there is no validation to ensure that the sum of all
requests.debtAmountvalues equalstotalDebtAmount. IftotalDebtAmountexceeds the sum ofrequests.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. -
L-20 Low Unvalidated Token Addresses Validation Acknowledged
Description
During Vanir actions, token addresses are not validated. A user could set
isEthto true while providing a token address other thanWETH. 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
isEthis true.
Remediation Review
8 findings · January 20, 2025-
C-01 Critical Unvalidated Parameters Allow an Attacker to Drain User Funds Validation Resolved
Description
The
executeOperationfunction 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>)whereparamswill 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 thelendingPoolAddressis truly whitelisted and not malicious.Recommendation
Ensure that the
lendingPoolAddressand theswapRouterAddressprovided are whitelisted by theVanircontract. -
H-01 High Unrestricted Direct Contract Interaction Validation Resolved
Description
The
Vanircontract is designed to work primarily via a centralized frontend. The expectation is that the backend logic will always supply “safe” parameters such as a legitimatelendingPoolAddress,swapRouterAddress,feeAddress,feeAmount, etc. However, there is no on-chain restriction preventing a user from calling theVanircontract directly providing arbitrary parameters.Therefore, malicious users can pass a malicious contract as
lendingPoolAddressorswapRouterAddress. 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
feeAmountandfeeAddressin actions likesetuporinterest. An attacker who directly calls the contract can simply set thefeeAmountto 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.
-
H-02 High Liquidations Will Revert When Flashloan Premium Is Different Than 0 Logical Error Resolved
Description
After a flashloan is initiated, AAVE performs a callback through the
executeOperationfunction passing as parameters the loanamountandpremiumwhich are the fees to be paid.However, in Vanir's
executeOperation, onlyamountis passed toprocessLiquidation. As a result, there won't be sufficient loan token swapped to repay the flashloan, and the entire liquidation process will fail.Recommendation
Add
premiumtoamountwhen calling theprocessLiquidationfunction:uint swapCost = processLiquidation(user, asset, amount + premium, lendingPoolAddress, collateralAsset); -
M-01 Medium
USDTswap is blocked DoS ResolvedDescription
In the
swap()function Vanir approves theswapRouterAddressto spendamountIntokens. 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 theUSDTtoken which doesn't support changing allowances from a non-zero value to another non-zero value.Recommendation
Consider clearing the approval to the
swapRouterAddressafter the swap concludes. -
L-01 Low Owner cannot be admin Code Best Practices Resolved
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.
-
L-02 Low Typo Code Best Practices Resolved
Description
The error message inside
Vanir.setAdmin()is"vanir/owner cannot change its own adminstatus.". There is no spacing betweenadminandstatus.Recommendation
Add a spacing between the two words
adminandstatus. -
L-03 Low Token And spToken Can Mismatch Validation Acknowledged
Description
During the withdrawal process, both
tokenAddressandspTokenAddressare 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
tokenAddressandspTokenAddressare corresponding addresses. -
L-04 Low Validate Asset Is Not Used Twice Validation Acknowledged
Description
M-01was acknowledged as intended behaviour, but the functionloan()has no access restriction and can be called by any user. This can lead to the issue fromM-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 callingloan()with multiple occurrences of the same asset.
Remediation Review 2
9 findings · February 8, 2025-
M-01 Medium User Can Pass Arbitrary Fee Amount Logical Error Acknowledged
Description
Function
borrowaccepts an arbitraryfeeAmountthrough aloancall. If Vanir expects a fee to be paid on borrow, a user can just directly interact with the contract and pass afeeAmountof zero to preserve as much capital.Recommendation
Add validation on the expected
feeAmountfor 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). -
M-02 Medium Failed Approvals With USDT Logical Error Resolved
Description
Function
repayapprovesamountand then calls SparkLend to repay the amount. The issue is that thepaybackAmountwithin the BorrowLogic is not necessarily equal toamountpassed, 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
forceApprovewhich 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. -
M-03 Medium Lost borrow fee Validation Acknowledged
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. -
L-01 Low LendingPoolAddress Points To Registry Instead Deployment Resolved
Description
The deployment script currently passes
0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE(thePoolAddressesProviderof Spark) to theVanirconstructor, instead of passing0xC13e21B648A5Ee794902342038FF3aDAB66BE987(the SparkLendingPooladdress 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 thePoolAddressesProvideraddress in the script. Using thePoolAddressesProviderdirectly is incorrect in this context because Vanir specifically needs the activeLendingPoolto perform supply/borrow/repay operations, not just an address provider.Recommendation
Update the constructor call in DeployVanir (or any relevant deployment script) to use
0xC13e21B648A5Ee794902342038FF3aDAB66BE987instead of0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE. -
L-02 Low No Controls On Liquidation Fee Amount Validation Resolved
Description
During liquidation, the user pays an additional
feeAmountfrom their collateral. ThisfeeAmountis 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
feeAmounton liquidations, or clearly document the fee calculation behavior of your backend system. -
L-03 Low Failed Liqudiations With Low Target LTV Warning Resolved
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.
-
L-04 Low Failing Tests Warning Acknowledged
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:
- Lack of pinned fork block number
- Improper Target LTV's for partial liquidations
- 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.
-
L-05 Low No events emitted on funds rescue Event Acknowledged
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.
-
L-06 Low Frozen Pool Will Lead To Fail Liquidations Warning Acknowledged
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 functionswap()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-
L-01 Low Incorrected Addresses Warning Acknowledged
Description
File
deploy-tenderly.jsonhas an incorrectlendingPooladdress (currently PoolAddressProvider) andfeesAddress(currently the SparkLend Pool).Recommendation
Update the addresses to
0xC13e21B648A5Ee794902342038FF3aDAB66BE987and0xc7453Ea3051eE01639eF13D67BF7d6a077ee77Dcrespectively. -
L-02 Low Leftover SwapRouter Approval Warning Acknowledged
Description
After function
swapis performed, the SwapRouter may still have some token approval leftover if the entireamountInwas not utilized.Recommendation
Remove the leftover approval after the swap.
-
L-03 Low Overcharge in executeOperation Logical Error Acknowledged
Description
As we know from the previous round, the
SparkLend.repay()function may not use all of the tokens we pass asamount. InexecuteOperationthe contract repays with the flashloaned tokens, withdraws collateral and swaps it for the flashloaned amount + premium. If therepay()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.
No findings match.
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.