Plots

Smart Contract Audit Report

Audit Summary

Plots Audit Report Plots Finance is building a new platform where NFTs can be listed and borrowed.

For this audit, we reviewed the project team's PlotsCore, PlotsTreasury, PlotsLend, NFTLoan, and RewardsReceiver contracts at commit e3c6b0e9cf7ea2f0ad08ff7e805737b33a5f3493 on the team's GitHub repository.

Audit Findings

All findings have been resolved, though some centralized aspects are present.
Date: January 12th, 2024.
Updated: February 23rd, 2024 to reflect updates made to the contract from commit 43d2c1573127ab57a74e94680bee2ade3e2c80fa to commit 618f8aa1a9dbe041c9821ae4d6296a457957cd1a.
Updated: March 21st, 2024 to reflect updates made to the contract from commit 618f8aa1a9dbe041c9821ae4d6296a457957cd1a to commit e3c6b0e9cf7ea2f0ad08ff7e805737b33a5f3493.

Finding #1 - PlotsCore/NFTLoan - High (Resolved)

Description: The ChangeOwnershipPercentage() function does not change the 'OwnershipType' state variable in the NFTLoan contract to reflect the updated ownership percentage. The Borrower of a loan whose ownershipType is 25% can repeatedly call the ChangeOwnershipPercentage() function passing in a 10% ownershipType.
Risk/Impact: The borrower will receive a refund in ETH with each call, draining the Treasury contract of its ETH balance.
Recommendation: The 'OwnershipType' state variable should be updated in the NFTLoan contract to reflect the updated ownership percentage.
Resolution: The team has removed the ChangeOwnershipPercentage() function from the contract.

Finding #2 - PlotsCore - High (Resolved)

Description: The ChangeOwnershipPercentage() function allows any user to specify an arbitrary loanContract address when calling the function. Users can pass in a malicious contract for the loanContract parameter that is built to pass each of the function's require statements.
Risk/Impact: The malicious contract could repeatedly change its ownership percentage from 25% to 10% to drain the PlotsTreasury contract of its ETH balance.
Recommendation: The team should ensure that the loanContract passed into the function is a contract that has been created in the BorrowToken() function by checking the 'IsLoanContract' mapping to prevent the use of arbitrary contracts.
Resolution: The team has removed the ChangeOwnershipPercentage() function from the contract.

Finding #3 - PlotsCore - High (Resolved)

Description: The CloseLoan() function allows any user to specify an arbitrary loanContract address when calling the function. Users can pass in a malicious contract for the loanContract parameter that is built to pass each of the function's require statements. The malicious contract could reference a valid Collection address and NFT ID that is currently part of the platform.
Risk/Impact: Malicious contract(s) have the potential to unauthorizedly close loans, consequently draining the ETH balance of the PlotsTreasury contract with each call.
Recommendation: The team should ensure that the loanContract passed into the function is a contract that has been created in the BorrowToken() function by checking the 'IsLoanContract' mapping to prevent the use of arbitrary contracts.
Resolution: The team has implemented the above recommendation.

Finding #4 - PlotsCore - High (Resolved)

Description: The ChangeOwnershipPercentage() function is vulnerable to reentrancy attacks. Reentrancy is made possible through the transfer of ETH to the Borrower address in the event that the Borrower address is a contract.
Risk/Impact: A malicious contract can execute arbitrary logic in its fallback function and reenter the ChangeOwnershipPercentage() function and drain the Treasury contract of its ETH balance. The user could then purchase VLND tokens at a deflated price in the PlotsTreasury contract.
Recommendation: The logic in the ChangeOwnershipPercentage() function should be restructured to follow the Checks-Effects-Interactions pattern as follows. The UpdateBorrowerRewardShare() function should be called before sending any ETH assuming that the UpdateBorrowerRewardShare() function properly updates the 'OwnershipType' state variable (Finding #1).
NFTLoan(LoanContract).UpdateBorrowerRewardShare(Ownership);

uint256 CurrentValue = PlotsTreasury(Treasury).GetTokenValueFloorAdjusted(NFTLoan(LoanContract).TokenCollection(), NFTLoan(LoanContract).TokenID());
uint256 CollateralValueChange;

if(CurrentOwnership == OwnershipPercent.Ten){
      //15% Inclusive of a 1% fee
      CollateralValueChange = (CurrentValue * 16) / 100;
      require(msg.value >= CollateralValueChange, "Not enough ether sent");
}
else if(CurrentOwnership == OwnershipPercent.TwentyFive){
      //15% Inclusive of a 1% fee
      CollateralValueChange = (CurrentValue * 14) / 100; 
      PlotsTreasury(Treasury).SendEther(payable(NFTLoan(LoanContract).Borrower()), CollateralValueChange);
}
Resolution: The team has removed the ChangeOwnershipPercentage() function from the contract.

Finding #5 - PlotsLend - High (Resolved)

Description: In the DepositToken() function, when autolist is true, the AutoList() function within the PlotsCoreContract is called. However, it erroneously uses address(this) as the 'User' parameter instead of msg.sender. This approach is inconsistent with the ListToken() function's handling, leading to inconsistency in specifying the user.
Risk/Impact: The incorrect specification of the lister's address will lead to an erroneous setting of the Owner address in the NFTLoan contract. Consequently, the loan cannot be delisted because the LendContract lacks the capability to call the Delist function.
Recommendation: In the DepositToken() function, msg.sender should be passed in for the 'User' parameter for the AutoList() function.
Resolution: The team has implemented the above recommendation.

Finding #6 - PlotsTreasury - Medium (Resolved)

Description: The BuyVLND() and SellVLND() functions may be susceptible to front-running.
Risk/Impact: A malicious user can preemptively buy tokens before a large buy transaction to take advantage of expected price increases. Similarly, the user can preemptively sell tokens ahead of a substantial sell order, to secure a higher selling price.
Recommendation: The BuyVLND() and SellVLND() functions should be modified to allow users to specify a minimum amount out of tokens and ETH when buying or selling.
Resolution: The team has implemented the above recommendation.

Finding #7 - Plots - Informational (Resolved)

Description: The following require statement that appears in the RenewLoan(), and EndLoan() functions is redundant as the function uses the OnlyManager modifier.
require(msg.sender == Manager, "Only Loans Or Treasury Contract can interact with this contract");
Recommendation: The above require statement could be removed from each function for additional gas savings on each call.
Resolution: The team has implemented the above recommendation.

Finding #8 - Plots - Informational (Resolved)

Description: The following state variables can only be set one time but are not declared immutable:
NFTLoan.Manager, PlotsCore.LendContract, PlotsCore.Treasury, PlotsLend.PlotsCoreContract, PlotsTreasury.PlotsCoreContract
Recommendation: The above state variables could be declared immutable for additional gas savings on each reference.
Resolution: The team has implemented the above recommendation.

Contracts Overview

PlotsCore Contract:
  • The deployer will specify a list of Admin addresses and a Fee receiver address.
  • Any Admin address can add a new Collection to the contract at any time.
  • Any user can specify a valid Collection and NFT ID to list at any time.
  • If the caller is an Admin, the owner of the specified NFT must be the PlotsTreasury contract. The Treasury contract is set as the Lister.
  • If the caller is not an Admin, the owner of the specified NFT must be the PlotsLend contract and the deposit must have been initiated by the caller. The caller's address is set as the Lister.
  • Any user that has listed an NFT can delist it at any time. If the Lister address is set to the Treasury contract, the caller must be an Admin address.
  • Any Admin address can set the Treasury contract address and Lend contract address one time.
  • Any user can borrow an NFT by specifying a Collection, NFT ID, duration of either 3 or 6 months, and ownership percentage of either zero, ten, or twenty-five percent.
  • If any Loan contracts are currently available, the last contract in the list of available contracts is used. Otherwise, a new NFTLoan contract is created.
  • If the listing type assigned to the NFT is 'Ownership', the value of the NFT is retrieved via the PlotsTreasury contract and a borrow cost value is calculated based on the desired ownership percentage.
  • The caller must provide an amount of ETH that either meets or exceeds the borrow cost value. 2% of the provided ETH is sent to the Fee Recipient address and the remainder is sent to the Treasury.
  • The NFT is transferred from the PlotsTreasury contract to the NFTLoan contract and its location is updated accordingly.
  • If the listing type assigned to the NFT is 'Usage', the specified ownership percentage must be zero and the location of the NFT must be the PlotsLend contract.
  • The NFT is transferred from the PlotsLend contract to the NFTLoan contract and the listing is removed on behalf of the lister.
  • Any Borrower address of an NFTLoan contract can change an existing ownership percent of an active loan to a new one at any time.
  • The change in collateral value required for the new ownership percentage is calculated. This calculation depends on the current ownership percentage and the token's value retrieved from the PlotsTreasury contract.
  • If the current ownership percentage is ten, the caller must provide an amount of ETH to support the calculated collateral value.
  • If the current ownership percentage is twenty-five, the borrower receives an ETH refund from the PlotsTreasury contract.
  • Any Borrower address of an NFTLoan contract can extend the end time of an active loan by either 3 months or 6 months at any time.
  • Any user can specify a list of Loan contracts and Reward token addresses to disperse rewards in each contract at any time.
  • A Borrower or Owner of an NFTLoan contract or an Admin can specify a Loan contract to close. The end time associated with the loan must have passed or the caller must be an Admin address in order for the transaction to successfully occur.
  • If the Ownership percentage associated with the loan is zero, the NFT is transferred from the NFTLoan contract to the PlotsCore contract and the loan details are reset to their default values.
  • If the Ownership percentage associated with the loan is ten or twenty-five, the NFT is transferred from the NFTLoan contract to the PlotsTreasury contract. A collateral value in ETH is calculated based on the ownership percentage and is transferred from the Treasury to the Borrower.
  • Any Admin address can set the Reward fee to any value up to 15% at any time.
  • Any Admin address can set the Fee Recipient to any address at any time.
PlotsTreasury Contract:
  • Any user can initiate a purchase of VLND tokens by providing an amount of ETH. The number of VLND tokens minted to the user is based on the amount of provided ETH and current calculated price per token. This value must exceed the minimum amount specified by the caller.
  • Any user can specify a number of VLND tokens to sell at any time. The specified number of tokens are transferred from the caller to the contract and are subsequently burned. The caller must grant the contract a sufficient allowance in order for the transfer to successfully occur.
  • An amount of ETH is transferred from the contract to the caller based on the current calculated price per token. This value must exceed the minimum amount specified by the caller.
  • Any Admin or the PlotsCore contract can initiate a deposit by specifying a Collection address, any owned NFT ID, and a price in ETH. Multiple deposits may be initiated in a single transaction.
  • The specified NFT is transferred from the caller to the contract.
  • The token floor factor for the Collection and NFT ID is stored based on the specified price in relation to the floor price of the Collection.
  • The Collection address must have been assigned a floor price in order for the transaction to successfully occur.
  • Any Admin or the PlotsCore contract can specify a Collection and NFT ID to withdraw from the contract at any time. Multiple withdrawals may be initiated in a single transaction
  • If the NFT is currently listed in the PlotsCore contract, it is automatically delisted.
  • Any Admin or the PlotsCore contract can withdraw any amount of unlocked ETH from the contract to a recipient address at any time.
  • Any Admin or the PlotsCore contract can withdraw any tokens from the contract to a recipient address at any time.
  • Any Admin or the PlotsCore contract can set the floor price for a list of added collections at any time. The total value locked in each Collection is adjusted to ensure all values reflect the latest pricing changes.
  • Any Admin can set the VLND token address referenced in the contract to any address at any time.
  • The PlotsCore address can use this contract to transfer a specified NFT to a Loan contract address at any time.
NFTLoan Contract:
  • The Manager address can begin a loan by specifying the Ownership percentage, owner, borrower, Collection address, NFT ID, duration, and initial token value.
  • The details of the loan are stored and the reward share for the borrower is set based on the specified ownership percentage.
  • The Manager address can specify a time duration to extend an active loan by at any time.
  • The Manager address can end an active loan at any time. The NFT associated with the loan is transferred from the contract to the specified origin address and the loan details are reset to their default values.
  • The owner of the loan, the borrower of the loan, or the Manager can specify a Reward token and disperse rewards at any time.
  • The contract must hold at least 1 reward token in order for the transaction to successfully occur.
  • A reward fee is charged and sent from the contract to the Fee Receiver address set in the PlotsCore contract.
  • The remaining Reward token balance is transferred from the contract to the Owner.
PlotsLend Contract:
  • Any user can specify an NFT Collection address and any of their owned NFT IDs to deposit into the contract at any time. The caller will specify whether or not to autolist the NFT in the PlotsCore contract. Users may elect to deposit multiple NFTs in a single transaction.
  • Any user can withdraw any of their previously deposited NFTs from the contract as long as it is not listed in the PlotsCore contract. Users may elect to withdraw multiple NFTs in a single transaction.
  • The PlotsCore contract can trigger a send-to-loan transaction at any time which will transfer the specified NFT to the NFTLoan contract and update the NFT location as the NFTLoan contract.
  • The PlotsCore contract can trigger a return-from-loan transaction at any time which will update the NFT location as this contract.
RewardsReceiver Contract:
  • The deployer will specify a list of addresses that will be signed as Admins upon deployment.
  • Any user can initiate the sending of rewards by specifying a list of loan addresses, token addresses, and reward amounts.
  • The Reward data is recorded and the payment is stored in the contract.
  • The specified number of rewards tokens are transferred from the caller to the contract in the form of the specified token. The caller must grant the contract a sufficient allowance in order for the transfer to successfully occur.
  • Any Admin address can withdraw any tokens from the contract at any time.
  • Any Admin address can set the rewards per loan value to zero for any loan and token address combination at any time.

Audit Results

Vulnerability Category Notes Result
Arbitrary Jump/Storage Write N/A PASS
Centralization of Control Any Admin address can close any loan before its end time has been reached in the PlotsCore contract. PASS
Compiler Issues N/A PASS
Delegate Call to Untrusted Contract N/A PASS
Dependence on Predictable Variables N/A PASS
Ether/Token Theft N/A PASS
Flash Loans N/A PASS
Front Running N/A PASS
Improper Events N/A PASS
Improper Authorization Scheme N/A PASS
Integer Over/Underflow N/A PASS
Logical Issues N/A PASS
Oracle Issues N/A PASS
Outdated Compiler Version N/A PASS
Race Conditions N/A PASS
Reentrancy N/A PASS
Signature Issues N/A PASS
Sybil Attack N/A PASS
Unbounded Loops N/A PASS
Unused Code N/A PASS
Overall Contract Safety   PASS

Inheritance Chart

Smart Contract Audit - Inheritance

Function Graph

Smart Contract Audit - Graph

Functions Overview


 ($) = payable function
 # = non-constant function
 
 Int = Internal
 Ext = External
 Pub = Public

 +  PlotsCoreV1 
    - [Pub]  #
    - [Pub] BorrowToken ($)
    - [Pub] CloseLoan #
    - [Pub] ChangeOwnershipPercentage ($)
    - [Pub] RenewLoan ($)
    - [Pub] ListToken #
    - [Pub] DelistToken #
    - [Pub] ClaimRewards #
    - [Pub] GetCollections
    - [Pub] GetCollectionListings
    - [Pub] IsListed
    - [Pub] GetSingularListing
    - [Pub] GetListedCollections
    - [Pub] GetOwnershipByPurchase
    - [Pub] GetRewardTokenClaimants
    - [Pub] GetRewardTokenPayouts
    - [Pub] GetUserLoans
    - [Pub] GetUserBorrows
    - [Pub] GetUserListings
    - [Pub] GetListedCollectionWithPrices
    - [Int] AddListingToCollection #
    - [Int] RemoveListingFromCollection #
    - [Int] AddListingToUser #
    - [Int] RemoveListingFromUser #
    - [Int] AddLoanToBorrowerAndLender #
    - [Int] RemoveLoanFromBorrowerAndLender #
    - [Pub] ChangeFeeReceiver #
       - modifiers: OnlyAdmin
    - [Pub] ChangeRewardFee #
       - modifiers: OnlyAdmin
    - [Pub] AddCollection #
       - modifiers: OnlyAdmin
    - [Pub] RemoveCollection #
       - modifiers: OnlyAdmin
    - [Pub] UpdateBorrowerPayoutTracker #
    - [Pub] UpdateOwnerPayoutTracker #

 +  PlotsTreasuryV1 
    - [Pub]  #
    - [Pub] BuyVLND ($)
    - [Pub] SellVLND #
    - [Pub] DepositNFT #
       - modifiers: OnlyAdmin
    - [Pub] DepositNFTs #
       - modifiers: OnlyAdmin
    - [Pub] WithdrawNFT #
       - modifiers: OnlyAdmin
    - [Pub] WithdrawNFTs #
       - modifiers: OnlyAdmin
    - [Pub] SendEther #
       - modifiers: OnlyAdmin
    - [Pub] SendERC20 #
       - modifiers: OnlyAdmin
    - [Pub] SetFloorPrice #
       - modifiers: OnlyAdmin
    - [Pub] SetVLND #
       - modifiers: OnlyAdmin
    - [Ext] SendToLoan #
       - modifiers: OnlyCore
    - [Ext] ReturnedFromLoan #
       - modifiers: OnlyCore
    - [Int] AddTokenToCollection #
    - [Int] RemoveTokenFromCollection #
    - [Pub] GetTotalValue
    - [Pub] GetFloorPrice
    - [Pub] GetFloorFactor
    - [Pub] GetTokenValueFloorAdjusted
    - [Pub] GetVLNDPrice
    - [Pub] GetVLNDInCirculation
    - [Ext]  ($)

 +  PlotsLendV1 
    - [Pub]  #
    - [Pub] DepositToken #
    - [Pub] DepositTokens #
    - [Pub] WithdrawToken #
    - [Pub] WithdrawTokens #
    - [Ext] SendToLoan #
       - modifiers: OnlyCore
    - [Ext] ReturnedFromLoan #
       - modifiers: OnlyCore
    - [Pub] GetUserTokens
    - [Pub] GetTokenDepositor
    - [Pub] GetTokenLocation

 +  NFTLoan 
    - [Pub]  #
    - [Pub] BeginLoan #
       - modifiers: OnlyManager
    - [Pub] RenewLoan #
       - modifiers: OnlyManager
    - [Pub] EndLoan #
       - modifiers: OnlyManager
    - [Pub] DisperseRewards #
    - [Pub] GetUnclaimedRewards
    - [Pub] UpdateBorrowerRewardShare #
       - modifiers: OnlyManager

 + [Int] ERC721 
    - [Ext] balanceOf
    - [Ext] ownerOf
    - [Ext] safeTransferFrom ($)
    - [Ext] transferFrom ($)
    - [Ext] approve ($)
    - [Ext] setApprovalForAll #
    - [Ext] getApproved
    - [Ext] isApprovedForAll

 + [Int] ERC20 
    - [Ext] balanceOf
    - [Ext] allowance
    - [Ext] approve #
    - [Ext] transfer #
    - [Ext] transferFrom #
    - [Ext] totalSupply
    - [Ext] Burn #

About SourceHat

SourceHat has quickly grown to have one of the most experienced and well-equipped smart contract auditing teams in the industry. Our team has conducted 1800+ solidity smart contract audits covering all major project types and protocols, securing a total of over $50 billion U.S. dollars in on-chain value!
Our firm is well-reputed in the community and is trusted as a top smart contract auditing company for the review of solidity code, no matter how complex. Our team of experienced solidity smart contract auditors performs audits for tokens, NFTs, crowdsales, marketplaces, gambling games, financial protocols, and more!

Contact us today to get a free quote for a smart contract audit of your project!

What is a SourceHat Audit?

Typically, a smart contract audit is a comprehensive review process designed to discover logical errors, security vulnerabilities, and optimization opportunities within code. A SourceHat Audit takes this a step further by verifying economic logic to ensure the stability of smart contracts and highlighting privileged functionality to create a report that is easy to understand for developers and community members alike.

How Do I Interpret the Findings?

Each of our Findings will be labeled with a Severity level. We always recommend the team resolve High, Medium, and Low severity findings prior to deploying the code to the mainnet. Here is a breakdown on what each Severity level means for the project:

  • High severity indicates that the issue puts a large number of users' funds at risk and has a high probability of exploitation, or the smart contract contains serious logical issues which can prevent the code from operating as intended.
  • Medium severity issues are those which place at least some users' funds at risk and has a medium to high probability of exploitation.
  • Low severity issues have a relatively minor risk association; these issues have a low probability of occurring or may have a minimal impact.
  • Informational issues pose no immediate risk, but inform the project team of opportunities for gas optimizations and following smart contract security best practices.