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

Security review · March 2024

NFT Lending Protocol

for Magnify Cash

The Magnify Cash team engaged Guardian to review the security of its NFT lending protocol. From the 19th of February to the 26th of February, a team of 4 auditors reviewed the source code in scope.

Published
Review window
February 19 to 26, 2024
Language
Solidity
Chains
Ethereum
Sector
Lending
  • 2 Critical
  • 2 High
  • 6 Medium
  • 9 Low
  • 0 Informational

14 resolved · 5 acknowledged

Scope

Overview

The Magnify Cash team engaged Guardian to review the security of its NFT lending protocol. From the 19th of February to the 26th of February, a team of 4 auditors reviewed the source code in scope.

Findings 19

  1. C-01 Critical Defaults Forced By Removing lendingDeskLoanConfigs DoS Resolved
    Location
    NFTYFinanceV1.sol

    Description

    With the removeLendingDeskLoanConfig function, a lending desk owner is able to remove the lendingDeskLoanConfig for loans that are still active. As a result the lending owner is able to prevent ERC1155 loans from being closed as nftCollectionIsErc1155 would be false for that lending desk and collection.

    Consequently, the makeLoanPayment function errantly attempts to treat ERC1155 tokens as ERC721 tokens and ultimately reverts.

    The lending desk owner can then subsequently add the correct lendingDeskLoanConfig back with the setLendingDeskLoanConfigs function only after the loan has expired and the owner can now claim the borrower’s collateral.

    Recommendation

    Do not read from the lendingDeskLoanConfigs mapping in the makeLoanPayment function, instead add an additional nftCollectionIsErc1155 boolean on the Loan struct and rely on that cached value to determine how to handle the transferring of collateral.

    Similarly, do not rely on the lendingDeskLoanConfigs mapping in the liquidateDefaultedLoan function, as the config may no longer be present. Instead rely on the new nftCollectionIsErc1155 boolean that will be stored on the Loan struct.

    Resolution

    Magnify Team: The issue was resolved in PR#230.

  2. C-02 Critical Frontrunning Loan Creations Frontrunning Resolved
    Location
    NFTYFinanceV1.sol: 579

    Description

    Each lending desk has a LoanConfig per nftCollection address, which contains the details about the minimum and maximum interest charged to the borrower.

    A malicious desk owner can front-run the initializeNewLoan call from the borrower, and change the loan configuration with a large interest rate and a small duration, making the user pay more interest than they expected to pay when the new loan transaction was originated.

    Currently, there is no validation for maxInterest besides maxInterest>=minInterest. If the desk owner sets the interest the max allowed interest type(uint32).max = 4294967295 and configures the other Loan params to have constant interest, the borrower Loan interest will be set to minInterest chosen by desk owner. This means that the borrower will pay around 4900% per interest per hour and there is a minimum of 1 hour wait to repay the loan.

    In summary, a malicious lender can set a small interest rate to honeypot borrowers, update the loan configuration before their transaction is initialized, and lock the borrower in at the max interest rate for at least an hour which they must pay.

    Recommendation

    Add an extra parameter to the initializedNewLoan function, where the borrower can set a maxInterestAllowed, which will act as a limit on what they are willing to pay.

    Resolution

    Magnify Team: The issue was resolved in PR#238.

  3. H-01 High Blacklisted Lenders Force Defaults DoS Resolved
    Location
    NFTYFinanceV1.sol: 822

    Description

    In the makeLoanPayment function, the lendingDesk.erc20 token is transferred directly to the lender address: IERC20(lendingDesk.erc20).safeTransferFrom(msg.sender, lender, _amount);

    Therefore any lender that is blacklisted for the payment token will prevent the user from making loan payments. The lender will then force the user to be liquidated as they cannot pay back their loan in time.

    Furthermore, in the case of tokens with hooks after transfers, like ERC777, the receiver can make the transfer revert, preventing the borrower to make any payments to the loan as well.

    Recommendation

    Do not push the lendingDesk.erc20 tokens directly to the lender address, instead increment a uint256 value in a mapping for an individual erc20 token and allow lenders to claim this amount with a separate function (pull-over-push pattern).

    Resolution

    Magnify Team: The issue was resolved in PR#258.

  4. H-02 High Interest Calculation Set To Min Interest Rounding Resolved
    Location
    NFTYFinanceV1.sol: 645-649

    Description

    When initializing a new loan, the user will pass both _duration and _amount parameters. If both amount and duration are variable, the interest should be calculated based on scaling both duration and amount.

    The issue arises due to the inherent rounding down behavior in Solidity. When calculating the average amount and duration, they are rounded down to 0. After multiplying with (loanConfig.maxInterest - loanConfig.minInterest), the result is 0. Consequently, the interest is always set as loanConfig.minInterest, resulting in the lender missing out on potential additional interest due to rounding down.

    • Example: If minAmount = 100, maxAmount = 200, and _amount=150
    • (_amount - loanConfig.minAmount) / (loanConfig.maxAmount - loanConfig.minAmount)
    • (150 - 100) / (200 - 100) = 50 / 100 = 0
    • The same happens with duration.

    Recommendation

    Calculate in the following manner to prevent rounding down:

    interest =
      loanConfig.minInterest +
      uint32(
          (// Take average of amount and duration factors
          (((_amount - loanConfig.minAmount) * (loanConfig.maxInterest - loanConfig.minInterest)) /
    (loanConfig.maxAmount - loanConfig.minAmount)) +
           (((_duration - loanConfig.minDuration) * (loanConfig.maxInterest - loanConfig.minInterest)) /
    (loanConfig.maxDuration - loanConfig.minDuration))
           ) / 2
      );
    

    Resolution

    Magnify Team: The issue was resolved in PR#239.

  5. M-01 Medium Overlap Between Payment And Default Periods Logical Error Resolved
    Location
    NFTYFinanceV1.sol: 741

    Description

    Since the hoursElapsed is rounded down in the getLoanAmountDue function and the loan.duration check uses strictly greater than, loan payments are only disabled an entire hour after the end date of a loan.

    Therefore during this time a borrower may still pay back their loans and may frontrun the liquidation tx to do so.

    Recommendation

    Alter the hoursElapsed > loan.duration check to be hoursElapsed >= loan.duration

    Resolution

    Magnify Team: The issue was resolved in PR#232.

  6. M-02 Medium Errant Origination Fee Validation Logical Error Resolved
    Location
    NFTYFinanceV1.sol: 888

    Description

    In the setLoanOriginationFee function the _loanOriginationFee basis points value is intended to be capped at a maximum of 10%, however the validation asserts that the _loanOriginationFee is less than 10_000, which represents 100% in basis points.

    Recommendation

    Validate that the _loanOriginationFee value is less than 1_000, rather than less than 10_000.

    Resolution

    Magnify Team: The issue was resolved in PR#240.

    .

  7. M-03 Medium Paused State Leads To Forced Defaults Logical Error Resolved
    Location
    NFTYFinanceV1.sol

    Description

    When the owner pauses the NFTYFinanceV1 contract, borrowers cannot repay their loans and therefore may be forced to default and lose their NFT collateral.

    Recommendation

    Consider allowing the makeLoanPayment function to be called when the protocol is paused.

    Resolution

    Magnify Team: The issue was resolved in PR#257.

  8. M-04 Medium Zero Platform Fee Can DoS New Loans DoS Resolved
    Location
    NFTYFinanceV1.sol: 721

    Description

    In the setLoanOriginationFee function there is not validation that the _loanOriginationFee is not 0, therefore the platformFee that is taken from new loans can be 0 when the loanOriginationFee is set to 0.

    Some ERC20 tokens choose to revert upon transferring a 0 amount, however there is no check that the platformFee is nonzero before attempting to transfer this amount to the platformWallet.

    Recommendation

    In the initializeNewLoan function, only attempt to transfer the platformFee to the platformWallet if the platformFee is nonzero.

    Resolution

    Magnify Team: The issue was resolved in PR#241.

  9. M-05 Medium Borrowers Exposed To Gas Griefing Gas Griefing Resolved
    Location
    NFTYFinanceV1.sol: 688

    Description

    In the initializeNewLoan function a promissoryNote is minted to the lender using the INFTYERC721V1.mint function, which relies on safeMint. As a result if the lender address is home to a contract, the onERC721Received function will be invoked at that address.

    The contract at the lender address may have malicious logic implemented for the onERC721Received function to waste the borrower’s gas, causing a loss of native tokens for the user.

    Recommendation

    Consider using _mint rather than _safeMint for the mint implementation in the NFTYERC721V1 contract so that borrowers cannot be exposed to gas griefing.

    Resolution

    Magnify Team: The issue was resolved in PR#253.

  10. M-06 Medium Block Stuffing Risk Block Stuffing Resolved
    Location
    NFTYFinanceV1.sol

    Description

    If the block.timestamp is a few seconds before the beginning of a new hour and User A sends a tx to pay back their full loan amount, the lender may stuff blocks on the network until the next hour begins. As a result, the borrowers debt to be repaid will increase and the borrowers tx will no longer close the loan.

    Immediately the borrower will have to pay an additional period of interest. More insidiously, the borrower may not realize that the loan remains open and as a result may be unexpectedly liquidated.

    Recommendation

    Consider allowing users to pass a boolean indicating whether they would like to repay the full amount, rather than always specifying a particular amount to pay back. This way the transaction will close the loan regardless of when the transaction is recorded.

    Resolution

    Magnify Team: The issue was resolved in PR#242.

  11. L-01 Low Misleading Comment Documentation Resolved
    Location
    NFTYFinanceV1.sol: 843

    Description

    In the liquidateDefaultedLoan function the loan status is assigned to Defaulted upon liquidation, however the comment on line 843 suggests that the loan state is assigned to resolved.

    Recommendation

    Update the comment to indicate that the loan.status will be assigned to LoanStatus.Defaulted rather than LoanStatus.Resolved.

    Resolution

    Magnify Team: The issue was resolved in PR#229.

  12. L-02 Low Inaccurate NatSpec Documentation Resolved
    Location
    NFTYERC721V1.sol: 114

    Description

    The function burn has the same NatSpec from the mint function.

    Recommendation

    Update the documentation to reflect the burn function accurately.

    Resolution

    Magnify Team: The issue was resolved in PR#243.

  13. L-03 Low Empty Loan Config Check Missing Validation Acknowledged
    Location
    NFTYFinanceV1.sol: 299

    Description

    The function setLendingDeskLoanConfigs is missing a check for empty loan config. Therefore it is possible to pass an empty _loanConfigs array but the LendingDeskLoanConfigsSet event will still be emitted.

    Recommendation

    Add a check to ensure _loanConfigs.length > 0

    Resolution

    Magnify Team: Acknowledged.

  14. L-04 Low Updating NFTY Finance Address Can DoS Centralization Risk Acknowledged
    Location
    NFTYERC721V1.sol: 89

    Description

    The NFTYERC721V1 contract, which is the base contract for NFTYLendingKeysV1, NFTYObligationNotesV1 and NFTYPromissoryNotesV1, has a function setNftyFinance which allows the owner to update the nftyFinance address.

    The main goal is to have the correct nftyFinance address set is to prevent unauthorized access to mint and burn function, meaning, only that contract is authorized to call them. In the scenario where the owner updates this address, existing loan desks and loanIds in the NFTFinanceV1 contract will be stuck, as the every call to burn will now fail, due to the onlyNftyFinance modifier.

    Recommendation

    Avoid updating nftyFinance when there are active loans.

    Resolution

    Magnify Team: Acknowledged.

  15. L-05 Low Lacking SafeCast Usage Best Practices Resolved
    Location
    NFTYFInanceV1.sol: 624, 642

    Description

    In the initializeNewLoan function the interest calculations include casting a uint256 to a uint32. These calculations should always be safe and avoid overflow, however as a best practice it would be prudent to use OpenZeppelin’s SafeCast library to perform these casts.

    Recommendation

    Consider implementing SafeCast for these uint32 casts.

    Resolution

    Magnify Team: Resolved.

  16. L-06 Low Interest Charged On Repaid Principle Unexpected Behavior Acknowledged
    Location
    NFTYFinanceV1.sol

    Description

    Interest on loans is charged on the original loan amount even if some of the loan principle has been repaid.

    Recommendation

    Be sure to clearly document this behavior to users.

    Resolution

    Magnify Team: Acknowledged.

  17. L-07 Low External Call Safety External Calls Resolved
    Location
    NFTYFinanceV1.sol

    Description

    Throughout the NFTYFinanceV1 contract external calls are made without regard to state updates, the following rules ought to be followed:

    • safeTransferFrom should occur first in functions to avoid making an external call via callback

    tokens after accounting has been updated but before funds have been received.

    • Other external calls should occur after all state updates have occurred.

    Recommendation

    Implemented the above suggestions, as seen in this PR:

    a Guardian proof of concept

    Resolution

    Magnify Team: The issue was resolved in PR#259.

  18. L-08 Low PUSH0 Warning Warning Acknowledged
    Location
    Global

    Description

    The Magnify Cash contracts are configured to user solidity 0.8.22 and higher, these versions of the EVM compiler make use of the PUSH0 opcode which is not supported by all EVM compatible chains.

    Recommendation

    The immediate deployment target of Ethereum Mainnet is safe as this network supports the PUSH0 opcode, however the team should be wary of PUSH0 support as they deploy to new EVM compatible networks.

    Before deploying to a new target chain, be sure to check whether the chain supports the PUSH0 opcode, and if it does not consider reducing the compiler version to < 0.8.20.

    Resolution

    Magnify Team: Acknowledged.

  19. L-09 Low System Incompatible With Fee-on-transfer Tokens Documentation Acknowledged
    Location
    Global

    Description

    Throughout the NFTYFinanceV1 contract the token transfer accounting assumes that the transferred amount is received, however this may not be the case for fee-on-transfer or rebase tokens.

    Recommendation

    Be sure to clearly document that the system is not compatible with fee-on-transfer tokens. Otherwise if they are intended to be supported then the amount of tokens actually received should be measured by a before and after balance check.

    Resolution

    Magnify Team: Acknowledged.

Invariants 21

The review's fuzzing suite asserted 21 invariants. 21 held.

Every invariant tested
IDInvariantResult
DESK-01Desk NFT sent to msg.senderHeld
DESK-02Desk balance increased with token depositsHeld
DESK-03Desk balance decreased with token withdrawalsHeld
DESK-04Owner can’t withdraw more than desk balanceHeld
DESK-05Only desk owner is able to update configsHeld
DESK-06Owner should only freeze desk when status is ActiveHeld
DESK-07Owner should only unfreeze desk when status is FrozenHeld
LOAN-01Interest is correctly calculated during loan initializationHeld
LOAN-02Loan status is Active after initializedHeld
LOAN-03Borrower token balance increased by loan amountHeld
LOAN-04Platform wallet balance increased by initialization feesHeld
LOAN-05Loans can’t be initialized when desk status is FrozenHeld
LOAN-06Loan paid back completely when resolve flag is setHeld
LOAN-07Borrower should get NFT back when loan is fully repaidHeld
LOAN-08Loan status is set to Resolved when fully repaidHeld
LOAN-09Loan should never be repaid after durationHeld
LOAN-10Only desk owner can liquidated loansHeld
LOAN-11Loans should only be liquidated after loan end timeHeld
LOAN-12Loans should not be liquidated if not in Active statusHeld
GLOBAL-01Token contract balance should always be greater of equal than all desk balancesHeld
GLOBAL-02Total borrowed amount of all loans should be less or equal to the total deposited amount in deskHeld

Put your code through the same review.

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

Get a quote