LandX Finance

QuantstampFiles named in the report

Quantstamp reviewed LandX Finance on 10 Nov 2022: 25 findings, 0 critical; 22 marked fixed, 1 acknowledged, 2 open.

No open critical findings
Open the reportOpen LandX Finance’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings25

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High005
Medium013
Low2014
High`rewardSharesPerToken` Is Not Always UpdatedFixed

When `rewardsToDistribute()` is called, it mints the vested tokens and increments the `rewardSharesPerToken`. However, the `rewardSharesPerToken` is not incremented if the vesting has fully completed at the time of the function call, as seen in the following code segment on L245: ```=solidity if (elapsedDays >= rewardVestingDuration) { amountVested = MAX_REWARD_AMOUNT.sub(rewardVested.amountVested); _mint(address(this), amountVested); rewardVested.amountVested += amountVested; rewardVested.lastVestedAt = block.timestamp; } ```

High`xBasket` Not Minted to ReceiverFixed

The specified receiver of the `_deposit()` function in `xBasket` never receives the minted xBasket tokens as intended. Additionally, the `deposit()` and `mint()` functions check whether the receiver is able to mint/deposit the requested amount whereas the funds are taken from the `msg.sender`.

HighIncorrect Rewards Accounting for Vested TokensFixed

The `LNDX` contract uses the global variables `rewardSharesPerToken` and `feeSharesPerToken` to track the amount of rewards and fees to distribute on a per token basis. To track how many rewards each user should receive, the contract stores the amount the user is not entitled to in the mappings `rewardsPerGrant` and `feePerGrant`. The simplified computation for the rewards of a user is then `amountOfTokens * rewardSharesPerToken - rewardsPerGrant`. Whenever a user claims rewards, `rewardsPerGrant` will therefore need to be updated to exclude the rewards that have just been claimed by adding them to `rewardsPerGrant`. The code however subtracts the claimed rewards from `rewardsPerGrant` (L198), which means the user will be able to claim even more tokens with each claim. Similarly, the code subtracts the fee that has been distributed from `feePerGrant` instead of adding it (L192). Additionally, the amount used is likely wrong since all tokens - including unvested ones - are used when computing the rewards.

HighMissing Support for Multiple CropsFixed

The `crop` state variable in `xToken` is set to the constant value, "CORN". The `xToken` constructor is designed to receive a value for the string of the crop being created, however, this is just used for the name of the ERC20 token, and not for the assignment of the `crop` state variable. This protocol is intended to work around multiple crops (e.g. xSOY, xWHEAT) however this constant state variable does not allow this as any reading of this state will read "CORN". Further, `xBasket` only considers four predetermined crops. While it is understood that these are the four crops that the `xBasket` will be beginning with, the team should have the functionality to add or remove crops used in the `xBasket` as needed.

HighSandwich AttacksFixed

A common attack in DeFi is the sandwich attack. Upon observing a trade of asset X for asset Y, an attacker front-runs the victim trade by also buying asset Y, lets the victim execute the trade, and then executes another trade after the victim by trading back the amount gained in the first trade. Intuitively, one uses the knowledge that someone is going to buy an asset, and that this trade will increase its price, to make a profit. The attacker’s plan is to buy this asset cheaply, let the victim buy at an increased price, and then sell the received amount again at a higher price afterward. The `getShards()` function of `xToken` converts both the shards mint fee and the `xTokensAnnualRent` from `xToken` to USDC through a Uniswap router. The `amountOutMinimum` and `sqrtPriceLimitX96` variables specified in the `ExactInputSingleParams` required for the Uniswap swap are set to 1, and 0 respectively, which will execute the swap even if there is one USDC returned to the user or a price of zero for the `xToken`. A savvy user could front-run these swaps to minimize the USDC returned to the `xToken` contract following the swap and profit off of the price change. This issue is also present in `xBasket.autoCompoundRewards()`, except they would be front-running USDC to `xToken` swaps.

MediumIdentical Token Symbols for cTokensFixed

The `cToken` constructor instantiates the ERC20 symbol of all cTokens created as "cCorn". This creates cTokens with identical symbols.

MediumMaximum Allowable Crop Share Not EnforcedFixed

The maximum allowable crop share value stored in `KeyProtocolVariables` is unused when minting a new LandXNFT. With nothing limiting the assignable value of the crop shares, LandXNFTs can be minted with crop share values that exceed the `maxAllowableCropShare` for the given crop as specified in `KeyProtocolVariables`.

MediumUnclear Precision UsedFixed

The contracts use different precision values throughout the code base without properly documenting them. Constants such as `10 ** 3`, `1e6`, `10 ** 7` and `10**9` are just some of the constants that can be found in the code. Without documentation, it is impossible to verify that the correct precision is used everywhere.

Low`totalAssets` Does Not Account for Potential YieldFixed

EIP-4626 specifies the following requirement for `totalAssets()`: > SHOULD include any compounding that occurs from yield. However, the current implementation of `totalAssets()` does not account for the potential yield.

Low`unstakePreview()` Returns Inaccurate ValuesFixed

`unstakePreview()` returns a tuple of the staked amount, rewards, and fees for a given `stakeID` if it were to be unstaked. However, the rewards value is not guaranteed to be accurate because `computeStakeReward()` is dependent on `rewardSharesPerToken`, which can be modified by the `rewardsToDistribute()` call in `unstake()`. The `previewUnstake()` function does not take into account possible changes to `rewardSharesPerToken` when `rewardsToDistribute()` is called.

Show 10 more
Low`xBasket` Auto Compound Potentially Vulnerable to Sandwich AttackFixed

Similar to the `xToken` sandwich attack issue, the `xBasket` also converts tokens using the UniswapV3 router without specifying a minimum amount out. Since sandwich attacks are only profitable for larger trades, it is unclear whether the yields from auto compounding are large enough to be at risk.

LowHardcoded Price Used for xTokenFixed

While it may be acceptable to use hardcoded prices pre-launch, these prices should not be used following the LandX launch. If the Uniswap pool for the queried xToken does not exist, these hardcoded values are used.

LowInconsistent Return Values for `calculateGrantClaim()`Fixed

`calculateGrantClaim()` currently behaves inconsistently. If the entire vesting duration has elapsed, it returns a tuple of the full vesting duration and any unclaimed rewards. If only part of the vesting duration has elapsed, it returns a tuple of the number of vested but unclaimed days, and the number of unclaimed rewards. An excerpt of the function is included below: ```=solidity // Check cliff was reached uint256 elapsedDays = block.timestamp.sub(grant.startTime - 1 days).div( 1 days ); // If over vesting duration, all tokens vested if (elapsedDays >= grant.vestingDuration) { uint256 remainingGrant = grant.amount.sub(grant.totalClaimed); return (grant.vestingDuration, remainingGrant); // <- Returns full vesting duration (of which only a subset may be unclaimed) } else { uint16 daysVested = uint16(elapsedDays.sub(grant.daysClaimed)); uint256 amountVestedPerDay = grant.amount.div( uint256(grant.vestingDuration) ); uint256 amountVested = uint256(daysVested.mul(amountVestedPerDay)); return (daysVested, amountVested); // <- returns only the unclaimed vested days } ```

LowIncorrect Version of SolidityFixed

The contracts currently use solidity version `0.8.6`. Due to a [medium severity bug](https://blog.soliditylang.org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/) in the solidity compiler since version `0.5.8`, this version is not in the [list of recommended versions](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity).

LowInherited Contract UnusedFixed

The `OraclePrices` contract inherits from both `Ownable` and `AccessControlEnumerable`. `Ownable` is unused and provides only a subset of the `AccessControlEnumerable` functionality and can thus be removed.

LowMissing Input ValidationFixed

It is important to validate inputs, even if they only come from trusted addresses, to avoid human error. The functions `updateXTokenMintFee()`, `updateCTokensSellFee()`, `updatePayRentFee()`, `updateMaxAllowableCropShare()`, `updateHedgeFundAllocation()`, `updateSecurityDepositMonths()` in `KeyProtocolVariables` do not impose bounds on the values being set. This allows for fees over 100% to be assigned which would result in the loss of user funds. A non-exhaustive list of functions missing validation includes: * `KeyProtocolVariables`: Confirm that the following functions update variables to reasonable values (Ex. confirm that percentages cannot be above 100%). * `updateXTokenMintFee()` * `updateCTokenSellFee()` * `updatePayRentFee()` * `updateHedgeFundAllocation()` * `nft.setBaseURI()`: Validate that `newuri` is not an empty string. * `OraclePrices.setGrainPrice()`: Confirm that the grain exists. * `OraclePrices.setXTokenPrice()`: Confirm that the xToken exists. * `xToken.preview()`: Perform the same require statements performed in `getShards()`.

LowNo Error Handling for Failed Grain Price AssignmentFixed

There is no error handling in the case that `oraclePrices.setGrainPrice()` fails in the Chainlink fulfill functions found in `OracleMulti`. Setting the grain price will fail if an invalid price was returned from the Chainlink request and the contract should react accordingly.

LowOwnership Can Be RenouncedFixed

If the owner renounces their ownership, all ownable contracts will be left without an owner. Consequently, any function guarded by the `onlyOwner` modifier will no longer be able to be executed.

LowRequire Message Deviates From Checked ConditionFixed

The function `grantLNDX()` checks that the `cliffInMonths` argument is less than five years when the require message states that it should be less than one year.

LowUnlocked PragmaFixed

Every Solidity file specifies in the header a version number of the format `pragma solidity (^)0.8.*`. The caret (`^`) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version _and above_, hence the term "unlocked".

Show 2 more
LowUnnecessary Use of `SafeMath` in Solidity 0.8.xFixed

Solidity 0.8.x has a built-in mechanism for dealing with overflows and underflows. There is no need to use the `SafeMath` library (it only increases gas usage).

LowUntraceable Contract EventsFixed

Staking and unstaking xTokens, as well as auto-compounding xBasket rewards, does not emit events and is therefore untraceable. This inhibits off-chain monitoring of these events.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (13)

  • contracts/xToken.sol
  • contracts/rentFoundation.sol
  • contracts/usdc.sol
  • contracts/cToken.sol
  • contracts/OraclePrices.sol
  • contracts/xTokenRouter.sol
  • contracts/LNDX.sol
  • contracts/veLNDX.sol
  • contracts/KeyProtocolVariables.sol
  • contracts/OracleMulti.sol
  • contracts/xBasket.sol
  • contracts/LNDXGovernor.sol
  • contracts/nft.sol

Part of LandX Finance’s security record

LandX Finance
Audits1 audit by 1 firmLast audit Nov 2022, by Quantstamp.
Critical findingsNo open critical findingsIn 1 report.
Open LandX Finance’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us