Skip to main content
Vine MoneyVine Money - March 04, 2024

Vine Money

Smart Contract Security Assessment

March 04, 2024

Vine_Money

SUMMARY


ABSTRACT

Dedaub was commissioned to perform a security audit of the Vine Money protocol. Vine Money is a lending protool, which is a fork of Prisma, to launch on the Sapphire ParaTime of the Oasis network. We were provided with documentation, but unfortunately, no test-suite was included.


BACKGROUND

The main changes in Vine Money compared to Prisma are:

  1. Inclusion of NaT Collateral: Vine Money introduces support for troves with ROSE, the native token of the Oasis network, as collateral.

  2. Deployment on Sapphire Chain: Unlike Prisma, Vine Money is deployed on the Sapphire chain of the Oasis network, which significantly enhances privacy. Data such as vUSD (the stablecoin minted by the protocol) balances and trove information are kept confidential.

  3. Utilization of Band Oracle: Vine Money opts for the Band oracle instead of Chainlink for its oracle needs.


SETTING & CAVEATS

This audit report mainly covers the contracts of the public repository https://github.com/VineMoney/VineMoney of the protocol at commit 48b55fc7d75f454574e9559b70732a8272b96a09. We performed the audit as a “delta” audit, inspecting the differences relative to commit c67c847d87c84c336900158caf662596b8fe984a of the Prisma codebase.

Two auditors worked on the codebase for two weeks on the following contracts:

VineMoney-main/

The audit’s main target is security threats, i.e., what the community understanding would likely call "hacking", rather than the regular use of the protocol. Functional correctness (i.e. issues in "regular use") is a secondary consideration. Typically it can only be covered if we are provided with unambiguous (i.e. full-detail) specifications of what is the expected, correct behavior. In terms of functional correctness, we often trusted the code’s calculations and interactions, in the absence of any other specification. Functional correctness relative to low-level calculations (including units, scaling and quantities returned from external protocols) is generally most effectively done through thorough testing rather than human auditing.


PROTOCOL-LEVEL CONSIDERATIONS

P1

Privacy considerations

PROTOCOL-LEVEL-CONSIDERATION
info

The Vine Money protocol emphasizes privacy in users' financial activities, achieving it to a certain extent. However, since the team did not provide us a detailed list of privacy promises or an adversarial model description, we refrain from categorizing privacy-related findings as issues or determining their severity. Nevertheless, we summarize here what is kept private, what is not, and the assumptions ensuring this privacy:

  • Privacy Enhancements for vUSD:
    vUSD, the stablecoin minted by the protocol, is an ERC20 token with significant privacy enhancements. Messages are not emitted on transfers, mints, burns, or approvals, allowing transaction confidentiality.

Read access to the balance of an address is restricted to the address itself and designated lookers, set by the protocol owner.Lookers are intended to be contracts needing balances for accounting purposes, ensuring privacy unless the owner acts dishonestly (refer to related issues N1 and H1).

  • Public Information about vUSD:
    Allowance from any owner to any spender is visible to any address, potentially indicating a relationship between the owner and spender. While allowances and balances are not directly correlated, non-zero allowances can imply such relationships.

  • Trove Data Access:
    Getter functions providing data about a trove (debt, collateral, stake) are accessible only to the trove owner or designated lookers (refer again to issue N1).

  • Public Total Status of Trove Managers:
    Variables concerning the total status of a trove manager, such as total collateral, total debt, total stakes, and total rewards, are publicly accessible. Adversaries can indirectly infer information about troves by monitoring these global variables and interactions with the protocol. For instance, observing changes in total system collateral before and after an address interacts with the protocol reveals collateral movements.

  • Info about stability pool LPs are public:
    The interactions of LPs with the stability pool are public. Messages are emitted on deposits and withdrawals and there are public mappings from every address to ist deposited collateral and rewards.

  • Collateral info can be retrieved from the sorted list of the troves:
    The findInsertPosition function within the SortedTroves contract receives a collateral ratio and calculates the addresses of two consecutive troves in the ordered list, with collateral ratios lower and higher than the provided ratio. This external function is accessible to any caller.Calling this function multiple times with various collateral ratios could enable an individual to estimate the collateral ratios of active troves. By observing the changes in the addresses returned by findInsertPosition for different input ratios, an attacker could deduce information about the distribution of collateral ratios among active troves.

  • Information leak through side channels:
    Side channels, like gas costs, can reveal info about a user's interaction with the protocol i.e. if the tx involved one branch or another. These attacks may be mitigated by gas padding (supported by Sapphire), but Vine Money protocol lacks measures against such attacks.



VULNERABILITIES & FUNCTIONAL ISSUES

This section details issues affecting the functionality of the contract. Dedaub generally categorizes issues according to the following severities, but may also take other considerations into account such as impact or difficulty in exploitation:

CATEGORY
DESCRIPTION
CRITICAL
Can be profitably exploited by any knowledgeable third-party attacker to drain a portion of the system’s or users’ funds OR the contract does not function as intended and severe loss of funds may result.
HIGH
Third-party attackers or faulty functionality may block the system or cause the system or users to lose funds. Important system invariants can be violated.
MEDIUM
Examples:
  • User or system funds can be lost when third-party systems misbehave
  • DoS, under specific conditions
  • Part of the functionality becomes unusable due to a programming error
LOW
Examples:
  • Breaking important system invariants but without apparent consequences
  • Buggy functionality for trusted users where a workaround exists
  • Security issues which may manifest when the system evolves

Issue resolution includes “dismissed” or “acknowledged” but no action taken, by the client, or “resolved”, per the auditors.


CRITICAL SEVERITY

[No critical severity issues]


HIGH SEVERITY

H1

Faulty implementation of authenticated modifier may compromise privacy

HIGH
resolved

Resolved

The team addressed the issue (at commits 617424e, 48b55f) by enforcing that the address which signed the message must coincide with the address being accessed.

The VineSignature::authenticated modifier controls the access to certain getter functions providing information about trove data, such as debt, collateral and stake or the vUSD balance of an address. Its purpose is to enable third parties to access trove information by providing a signed message from the trove owner. However, the current implementation is flawed. The modifier merely verifies that the address included in the signed message (auth.user) matches the address that signed the message.

VineSignature::authenticated()
modifier authenticated(SignIn calldata auth)    {
// Must be signed within 24 hours ago.
require( auth.time > (block.timestamp - (60*60*24)) );

// Validate EIP-712 sign-in authentication.
bytes32 authdataDigest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
SIGNIN_TYPEHASH,
auth.user,
auth.time
))
));

address recovered_address = ecrecover(
authdataDigest, uint8(auth.rsv.v), auth.rsv.r, auth.rsv.s);

require( auth.user == recovered_address, "Invalid Sign-In" );

_;
}

Nevertheless, there is no enforced relationship between this address and the address for which we seek information.

Consider for example the TroveManagerGetters::getTrove function.

TroveManagerGetters::getTrove()
function getTrove(SignIn calldata auth, address _troveManager, address _borrower) external authenticated(auth) view returns (
uint256 debt,
uint256 coll,
uint256 stake,
uint8 status,
uint128 arrayIndex,
uint256 activeInterestIndex
) {//Dedaub: auth and _borrower are independent
return ITroveManager(_troveManager).getTrove(_borrower);ITroveManager.getTrove
}

Anyone can sign a message with their own address in the auth.user field and invoke getTrove for any borrower. The modifier check will succeed, granting the third party unrestricted access to the borrower's trove information.

H2

Incorrect parameters passed to checkBalanceOf in TroveManager::redeemCollateral

HIGH
resolved

Resolved

Resolved at commit 1d6214c.

In the TroveManager::redeemCollateral function, there's a requirement to verify that the caller possesses a sufficient balance of debt tokens. This verification involves checking if the caller's debt token balance is greater than or equal to the amount they intend to redeem. To perform this check, the function invokes checkBalanceOf from the debt token contract, passing the msg.sender address as an argument. However, checkBalanceOf expects arguments in the form of a SignIn structure (or (SignIn calldata auth, address account) before the fix related to issue H1). Consequently, the call will fail.

There are two possible solutions of this issue:

  • Add an additional argument SignIn auth to the redeemCollateral function and invoke checkBalanceOf with this structure.

  • Instead of using checkBalanceOf, directly call balanceOf since there's no privacy compromise in this context. The TroveManager making the external call is a looker, and the balance being checked is that of the caller of redeemCollateral.

H3

Inadequate handling of NaT collateral in claimCollateralGains

HIGH
resolved

Resolved

Resolved at commit 2609a17.

In the StabilityPool::claimCollateralGains function, collateral gains for specified collateral indices are computed and sent to a Stability Pool liquidity provider. The function utilizes the safeTransfer function to send tokens, assuming that the collateral token is an ERC20 token.

StabilityPool::claimCollateralGains()
function claimCollateralGains(address recipient, uint256[] calldata collateralIndexes) public virtual {
_accrueDepositorCollateralGain(msg.sender);

uint256 loopEnd = collateralIndexes.length;
uint256[] memory collateralGains = new uint256[](collateralTokens.length);

uint80[256] storage depositorGains = collateralGainsByDepositor[msg.sender];
for (uint256 i; i < loopEnd; ) {
uint256 collateralIndex = collateralIndexes[i];
uint256 gains = depositorGains[collateralIndex];
if (gains > 0) {
collateralGains[collateralIndex] = gains;
depositorGains[collateralIndex] = 0;
collateralTokens[collateralIndex].safeTransfer(recipient, gains); //Dedaub: it is assumed that the collateral is an ERC20, although it could be NaT
}
unchecked {
++i;
}
}
emit CollateralGainWithdrawn(msg.sender, collateralGains);
}

However, there's a possibility that one of the specified collateral indices corresponds to the native collateral token (ROSE). In such cases, the transfer cannot be completed using safeTransfer, and the call method should be employed instead.



MEDIUM SEVERITY

M1

Vulnerability in the price feed algorithm for handling large price changes

MEDIUM
open

In the Vine Money protocol, the algorithm for updating prices in the price feed encounters difficulties recovering after a significant price change. The process begins with a call to the Band oracle, followed by checks to ensure the oracle's functionality, including successful retrieval of the price, non-zero return, and absence of staleness. If these checks fail, the function reverts. If successful, the oracle price is compared with the stored price in the contract, with the relative difference capped by a maximum deviation.

PriceFeed::_processFeedResponse()
function _processFeedResponses(
address _token,
OracleRecord memory oracle,
FeedResponse memory _currResponse,
PriceRecord memory priceRecord
) internal returns (uint256) {
bool isValidResponse = _isFeedWorking(_currResponse, oracle.heartbeat) && !_isPriceChangeAboveMaxDeviation(_currResponse, priceRecord);//Dedaub:the oracle response is compared to the stored price, but this is not an accurate test for the validity of the oracle price, especially if a long time has passed since the last time a price was stored in the contract
. . .
}

Unlike other protocols such as Prisma or Liquity, which utilize Chainlink oracles and compare prices between consecutive rounds, Vine Money compares the current oracle price with the stored price. While this approach is valid under normal circumstances, it lacks the ability to sufficiently gauge the validity of the oracle price. If a considerable amount of time has elapsed since the last interaction with the protocol, the difference between these two prices could be substantial without indicating an issue with the oracle price.

Although the maximum deviation is set to a large percentage (e.g., 50%), it is assumed that users will interact with the protocol before this threshold is exceeded. However, if this assumption fails, and the oracle price changes significantly, the protocol may become difficult to revive. For instance, if the oracle price changes by more than 50% compared to the stored price, and enough time has passed rendering the stored price stale, all checks in the fetchPrice function will fail, causing transactions to revert. If collateral prices stabilize around this new oracle price, the fetchPrice function will continue to revert, presenting a contrast with the behavior of protocols like Prisma or Liquity.

To address this vulnerability we suggest not just comparing the deviation of the oracle and stored prices, but also consider the time since the last update. If this is large then the comparison makes no sense and should be dismissed and probably the best practice in this case is to use the oracle price.



LOW SEVERITY

L1

Lack of access Control on setInitialParameters functions in multiple contracts

LOW
open

The contracts Factory, StabilityPool, DebtToken, VineToken and IncentiveVoting contain a setInitialParameters function intended to initialize critical variables for each respective contract. While these functions can be called only once, there exists no access control mechanism, allowing anyone to call them and set vital protocol variables at their discretion. Although it's anticipated that the protocol owner will execute these functions promptly post-deployment, such enforcement is absent from the code. Given the absence of benefits associated with leaving these parameters uninitialized, and considering the immutable nature of the variables defined in the setInitialParameters functions, we recommend declaring these variables as immutable and initializing them within the constructors of the contracts for improved security and control.

L2

Consider using .call instead of .transfer

LOW
open

Vine contracts currently use, in the BorrowerOperations::sendROSE, the .transfer method for native token (ROSE) transactions. This method forwards a fixed amount of gas (2300) and that was considered to be a measure against reentrancy. However, this approach assumed constant gas costs, which is not the case, since gas costs are subject to change. Any contract using .transfer takes a hard dependency on gas costs and could break after a future gas costs update. Moreover, the use of .transfer hampers interactions with other protocols that require multiple actions or adjustments to accounting variables upon receiving tokens from Vine. Additionally, it poses a challenge for Vine's interaction with accounts using account abstraction.

Since Vine already employs the checks-effects-interactions pattern for security, we believe that it is safe to replace .transfer with .call. This switch would not only eliminate gas cost dependencies but also improve interactions with other protocols.



CENTRALIZATION ISSUES

It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocol’s owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-profile, high-value protocols have significant centralization threats.)

N1

Potential privacy compromise for vUSD holders by contract owner

CENTRALIZATION
info

The vUSD contract allows read access to vUSD balances for addresses designated as lookers, primarily intended for protocol contracts requiring balance access for accounting purposes. However, the setLookers function, controlled by the contract owner, enables arbitrary designation of looker addresses. While it's assumed that only protocol contracts necessitating balance access will be designated as lookers, the absence of restrictions permits the owner to designate any address, potentially compromising the privacy of vUSD holders. The centralization concerns may be mitigated if the contract owner is a DAO, as expected.

N2

CENTRALIZATION
info

Info

The team addressed the issue by deploying a proxy contract and assigning it ownership of the VineCore contract and therefore of all associated contracts within the Vine protocol. This proxy contract is specifically designed to prevent calling the VineToken contract and the commitTransferOwnership function of VineCore (therefore this contract will be the permanent owner of the Vine protocol and all the owner-related actions should go through this contract that prevents calling the vulnerable functions). The current owner of the proxy contract is an EOA, however, the team has communicated their intention to transfer ownership to a DAO in the future.

The Vine Money protocol includes functions related to bridging Vine Tokens to other chains using the Celer Interchain protocol. However, the integration remains incomplete. Interacting with Celer requires deploying specific contracts with specified functions to both sides of the bridge. Unfortunately, these contracts are absent from the provided codebase for this audit.

The team has communicated that they do not have immediate plans to integrate with Celer or offer cross-chain functionality in the near future. Consequently, we recommend removing the functions setCelerEndpoint, receiveFromChain and burn. Otherwise, the owner can designate any address as a celerEndpoint, allowing them to mint or burn Vine Tokens from any address without constraints.

N3

Potential disruption to trove managers by protocol owner

CENTRALIZATION
info

Within the Vine Money protocol, the owner of the factory contract possesses the authority to enable and disable trove managers. While enabling trove managers aligns with operational needs, the ability to disable trove managers raises concerns.

When the owner decides to disable a trove manager, no conditions are enforced regarding the status of its open troves.

When a trove manager is disabled, critical functions such as burn and mint of the debt token become inaccessible. Consequently, trove owners are unable to perform essential actions such as closing their troves fully or partially, vUSD holders cannot redeem their debt tokens for collateral.



OTHER / ADVISORY ISSUES

This section details issues that are not thought to directly affect the functionality of the project, but we recommend considering them.

A1

Non-adherence to EIP-712 standard in VineSignature contract

ADVISORY
info

The VineSignature.sol contract is employed to verify message signatures in accordance with EIP-712. However, despite the message to be signed containing only the first two variables of the SignIn struct (address user and uint32 time), the computation of typeHash utilizes the name of the SignIn structure. To enhance compatibility with the EIP, we suggest defining a new structure containing solely these two variables (address user, uint32 time) and employing the name of this new structure in the calculation of typeHash.

A2

Enhanced safety measures for payable functions in BorrowerOperations

ADVISORY
info

In BorrowerOperations.sol, several functions enable users to deposit collateral on their troves: openTrove, addColl and adjustTrove. These functions are payable to accommodate the use of ROSE (the native token of the Oasis network) as collateral. To prevent user errors and ensure safety, it is advisable to implement additional sanity checks. Specifically, it is proposed to verify that when troveManagersData[troveManager].collateralToken is not equal to IERC20(ROSE), the msg.value is zero. This validation ensures that transactions reverting when users attempt to send ROSE to a trove manager with non-ROSE collateral, thus preventing potential loss of funds..

A3

Redundant type casting

ADVISORY
info

In PriceFeed.sol, there are two instances of unnecessary typecasting:

  • In _isPriceChangeAboveMaxDeviation, _currResponse.rate is already of type uint256.
  • In _fetchCurrentFeedResponse, _oracle.bandRecord is already of type IStdReference (and not an address), therefore, the casting IStdReference(_oracle.bandRecord) is not needed.

A4

Redundant boolean argument in ERC20::_approve

ADVISORY
info

The common implementations of the ERC20 contain two versions of the internal function _approve. The first version accepts as arguments (address owner, address spender, and uint256 value), while the second version includes an additional boolean argument. This boolean parameter indicates whether a message should be emitted or not. In the implementation of the ERC20 standard by the Vine Money team, no messages regarding approvals are emitted. Consequently, the boolean argument is redundant and should be removed. As a result of this modification, the two _approve functions will become identical, necessitating the removal of one of them to eliminate redundancy.

A5

Redundant condition in PriceFeed::_processFeedResponse

ADVISORY
info

In the PriceFeed contract's _processFeedResponses function, a boolean variable isValidResponse is computed based on two conditions:

PriceFeed::_processFeedResponses
bool isValidResponse = _isFeedWorking(_currResponse, oracle.heartbeat) &&//Dedaub: //whenever _processFeedResponses is called this is always true and can be removed            !_isPriceChangeAboveMaxDeviation(_currResponse, priceRecord);

However, _processFeedResponse is only called by fetchPrice if _isFeedWorking(_currResponse, oracle.heartbeat) is true. Therefore, the first term in the isValidResponse calculation, _isFeedWorking(_currResponse, oracle.heartbeat), is redundant

A6

Asymmetry in staleness condition and update times handling in PriceFeed

ADVISORY
info

The PriceFeed contract utilizes the Band Oracle, which stores USD prices for various assets along with their corresponding update times. When fetching a price from the oracle, both the base and quote assets must be specified, with the oracle returning the ratio of the base to the quote USD prices and the last update times of these two assets. The last update time of USD is always the timestamp of the block it was called.

In the PriceFeed contract, when a price is stored as the last update time in the contract, the base last update time is used (assuming the quote asset is USD). However, during the computation of _isFeedWorking, both the base and quote update times are checked. If the quote asset is USD as expected, the staleness condition regarding it becomes redundant.

A7

Unnecessary comparison in PriceFeed::_isFeedWorking

ADVISORY
info

The comparison _currentResponse.success == true can be rewritten as _currentResponse.success.

A8

ROSE address could be constant

ADVISORY
info

The address for ROSE in BorrowerOperations.sol is declared as immutable, even though it is not set in the constructor. It could be declared as a constant instead.

A9

Compiler bugs

ADVISORY
info

The code has the compile pragma ^0.8.19. For deployment, we recommend no floating pragmas, i.e., a fixed version, for predictability. Solc versions ^0.8.19 have some known bugs, which we do not believe to affect the correctness of the contracts.



DISCLAIMER

The audited contracts have been analyzed using automated techniques and extensive human inspection in accordance with state-of-the-art practices as of the date of this report. The audit makes no statements or warranties on the security of the code. On its own, it cannot be considered a sufficient assessment of the correctness of the contract. While we have conducted an analysis to the best of our ability, it is our recommendation for high-value contracts to commission several independent audits, a public bug bounty program, as well as continuous security auditing and monitoring through Dedaub Security Suite.


ABOUT DEDAUB

Dedaub offers significant security expertise combined with cutting-edge program analysis technology to secure some of the most prominent protocols in DeFi. The founders, as well as many of Dedaub's auditors, have a strong academic research background together with a real-world hacker mentality to secure code. Protocol blockchain developers hire us for our foundational analysis tools and deep expertise in program analysis, reverse engineering, DeFi exploits, cryptography and financial mathematics.