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
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
-
C-01 Critical Defaults Forced By Removing lendingDeskLoanConfigs DoS Resolved
Description
With the
removeLendingDeskLoanConfigfunction, a lending desk owner is able to remove thelendingDeskLoanConfigfor loans that are still active. As a result the lending owner is able to prevent ERC1155 loans from being closed asnftCollectionIsErc1155would be false for that lending desk and collection.Consequently, the
makeLoanPaymentfunction errantly attempts to treat ERC1155 tokens as ERC721 tokens and ultimately reverts.The lending desk owner can then subsequently add the correct
lendingDeskLoanConfigback with thesetLendingDeskLoanConfigsfunction only after the loan has expired and the owner can now claim the borrower’s collateral.Recommendation
Do not read from the
lendingDeskLoanConfigsmapping in themakeLoanPaymentfunction, instead add an additionalnftCollectionIsErc1155boolean on theLoanstruct and rely on that cached value to determine how to handle the transferring of collateral.Similarly, do not rely on the
lendingDeskLoanConfigsmapping in theliquidateDefaultedLoanfunction, as the config may no longer be present. Instead rely on the newnftCollectionIsErc1155boolean that will be stored on theLoanstruct.Resolution
Magnify Team: The issue was resolved in PR#230.
-
C-02 Critical Frontrunning Loan Creations Frontrunning Resolved
Description
Each lending desk has a
LoanConfigpernftCollectionaddress, which contains the details about the minimum and maximum interest charged to the borrower.A malicious desk owner can front-run the
initializeNewLoancall 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
maxInterestbesidesmaxInterest>=minInterest. If the desk owner sets the interest the max allowed interesttype(uint32).max = 4294967295and configures the other Loan params to have constant interest, the borrower Loan interest will be set tominInterestchosen 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
initializedNewLoanfunction, where the borrower can set amaxInterestAllowed, which will act as a limit on what they are willing to pay.Resolution
Magnify Team: The issue was resolved in PR#238.
-
H-01 High Blacklisted Lenders Force Defaults DoS Resolved
Description
In the
makeLoanPaymentfunction, thelendingDesk.erc20token 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.erc20tokens 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.
-
H-02 High Interest Calculation Set To Min Interest Rounding Resolved
Description
When initializing a new loan, the user will pass both
_durationand_amountparameters. 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, theinterestis always set asloanConfig.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.
- Example: If
-
M-01 Medium Overlap Between Payment And Default Periods Logical Error Resolved
Description
Since the
hoursElapsedis rounded down in thegetLoanAmountDuefunction and theloan.durationcheck 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.durationcheck to behoursElapsed >= loan.durationResolution
Magnify Team: The issue was resolved in PR#232.
-
M-02 Medium Errant Origination Fee Validation Logical Error Resolved
Description
In the
setLoanOriginationFeefunction the_loanOriginationFeebasis points value is intended to be capped at a maximum of 10%, however the validation asserts that the_loanOriginationFeeis less than 10_000, which represents 100% in basis points.Recommendation
Validate that the
_loanOriginationFeevalue is less than 1_000, rather than less than 10_000.Resolution
Magnify Team: The issue was resolved in PR#240.
.
-
M-03 Medium Paused State Leads To Forced Defaults Logical Error Resolved
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
makeLoanPaymentfunction to be called when the protocol is paused.Resolution
Magnify Team: The issue was resolved in PR#257.
-
M-04 Medium Zero Platform Fee Can DoS New Loans DoS Resolved
Description
In the
setLoanOriginationFeefunction there is not validation that the_loanOriginationFeeis not 0, therefore theplatformFeethat is taken from new loans can be 0 when theloanOriginationFeeis set to 0.Some ERC20 tokens choose to revert upon transferring a 0 amount, however there is no check that the
platformFeeis nonzero before attempting to transfer this amount to the platformWallet.Recommendation
In the
initializeNewLoanfunction, only attempt to transfer theplatformFeeto theplatformWalletif theplatformFeeis nonzero.Resolution
Magnify Team: The issue was resolved in PR#241.
-
M-05 Medium Borrowers Exposed To Gas Griefing Gas Griefing Resolved
Description
In the
initializeNewLoanfunction apromissoryNoteis minted to the lender using theINFTYERC721V1.mintfunction, which relies on safeMint. As a result if the lender address is home to a contract, theonERC721Receivedfunction will be invoked at that address.The contract at the lender address may have malicious logic implemented for the
onERC721Receivedfunction to waste the borrower’s gas, causing a loss of native tokens for the user.Recommendation
Consider using
_mintrather than_safeMintfor the mint implementation in theNFTYERC721V1contract so that borrowers cannot be exposed to gas griefing.Resolution
Magnify Team: The issue was resolved in PR#253.
-
M-06 Medium Block Stuffing Risk Block Stuffing Resolved
Description
If the
block.timestampis 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.
-
L-01 Low Misleading Comment Documentation Resolved
Description
In the
liquidateDefaultedLoanfunction the loan status is assigned toDefaultedupon 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.statuswill be assigned toLoanStatus.Defaultedrather thanLoanStatus.Resolved.Resolution
Magnify Team: The issue was resolved in PR#229.
-
L-02 Low Inaccurate NatSpec Documentation Resolved
Description
The function
burnhas the same NatSpec from themintfunction.Recommendation
Update the documentation to reflect the
burnfunction accurately.Resolution
Magnify Team: The issue was resolved in PR#243.
-
L-03 Low Empty Loan Config Check Missing Validation Acknowledged
Description
The function
setLendingDeskLoanConfigsis missing a check for empty loan config. Therefore it is possible to pass an empty_loanConfigsarray but theLendingDeskLoanConfigsSetevent will still be emitted.Recommendation
Add a check to ensure
_loanConfigs.length > 0Resolution
Magnify Team: Acknowledged.
-
L-04 Low Updating NFTY Finance Address Can DoS Centralization Risk Acknowledged
Description
The NFTYERC721V1 contract, which is the base contract for NFTYLendingKeysV1, NFTYObligationNotesV1 and NFTYPromissoryNotesV1, has a function
setNftyFinancewhich allows the owner to update thenftyFinanceaddress.The main goal is to have the correct
nftyFinanceaddress set is to prevent unauthorized access tomintandburnfunction, 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 theonlyNftyFinancemodifier.Recommendation
Avoid updating
nftyFinancewhen there are active loans.Resolution
Magnify Team: Acknowledged.
-
L-05 Low Lacking SafeCast Usage Best Practices Resolved
Description
In the
initializeNewLoanfunction the interest calculations include casting auint256to auint32. These calculations should always be safe and avoid overflow, however as a best practice it would be prudent to use OpenZeppelin’sSafeCastlibrary to perform these casts.Recommendation
Consider implementing
SafeCastfor theseuint32casts.Resolution
Magnify Team: Resolved.
-
L-06 Low Interest Charged On Repaid Principle Unexpected Behavior Acknowledged
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.
-
L-07 Low External Call Safety External Calls Resolved
Description
Throughout the NFTYFinanceV1 contract external calls are made without regard to state updates, the following rules ought to be followed:
safeTransferFromshould 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.
-
L-08 Low PUSH0 Warning Warning Acknowledged
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.
-
L-09 Low System Incompatible With Fee-on-transfer Tokens Documentation Acknowledged
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.
No findings match.
Invariants 21
The review's fuzzing suite asserted 21 invariants. 21 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
DESK-01 | Desk NFT sent to msg.sender | Held |
DESK-02 | Desk balance increased with token deposits | Held |
DESK-03 | Desk balance decreased with token withdrawals | Held |
DESK-04 | Owner can’t withdraw more than desk balance | Held |
DESK-05 | Only desk owner is able to update configs | Held |
DESK-06 | Owner should only freeze desk when status is Active | Held |
DESK-07 | Owner should only unfreeze desk when status is Frozen | Held |
LOAN-01 | Interest is correctly calculated during loan initialization | Held |
LOAN-02 | Loan status is Active after initialized | Held |
LOAN-03 | Borrower token balance increased by loan amount | Held |
LOAN-04 | Platform wallet balance increased by initialization fees | Held |
LOAN-05 | Loans can’t be initialized when desk status is Frozen | Held |
LOAN-06 | Loan paid back completely when resolve flag is set | Held |
LOAN-07 | Borrower should get NFT back when loan is fully repaid | Held |
LOAN-08 | Loan status is set to Resolved when fully repaid | Held |
LOAN-09 | Loan should never be repaid after duration | Held |
LOAN-10 | Only desk owner can liquidated loans | Held |
LOAN-11 | Loans should only be liquidated after loan end time | Held |
LOAN-12 | Loans should not be liquidated if not in Active status | Held |
GLOBAL-01 | Token contract balance should always be greater of equal than all desk balances | Held |
GLOBAL-02 | Total borrowed amount of all loans should be less or equal to the total deposited amount in desk | Held |
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.
