Halborn Logo

icon

DirectLoanFixedOffer Redeployment - NFTfi


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: October 31st, 2022 - November 7th, 2022

Summary

100% of all REPORTED Findings have been addressed

All findings

3

Critical

0

High

0

Medium

1

Low

0

Informational

2


1. INTRODUCTION

NFTfi engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-10-31 and ending on 2022-11-07. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn was provided one week for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended

    • Identify potential security issues with the smart contracts

In summary, Halborn identified some security risks that were mostly addressed by the NFTfi team.

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the bridge code and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:

    • Research into architecture and purpose

    • Smart contract manual code review and walkthrough

    • Graphing out functionality and contract logic/connectivity/functions (solgraph)

    • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes

    • Manual testing by custom scripts

    • Scanning of solidity files for vulnerabilities, security hotspots or bugs. (MythX)

    • Static Analysis of security for scoped contract, and imported functions. (Slither)

    • Testnet deployment (Brownie, Remix IDE)

4. SCOPE

IN-SCOPE: The security assessment was scoped to the following smart contracts:

    • DirectLoanFixedOfferRedeploy.sol

So then, they were also reviewed:

    • DirectLoanFixedOffer.sol

    • DirectLoanBaseMinimal.sol

    • DirectLoanBase.sol

    • PermittedERC20s.sol

    • BaseLoan.sol

    • NftReceiver.sol

    • LoanData.sol

Commit ID: 0e20d31354e394d35f1550becbd0990f85023dbd

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

1

Low

0

Informational

2

Impact x Likelihood

HAL-01

HAL-03

HAL-02

Security analysisRisk levelRemediation Date
POSSIBLE LOSS OF OWNERSHIPMediumRisk Accepted
ZERO ADDRESS NOT CHECKEDInformationalAcknowledged
USE I++ INSTEAD OF ++I IN LOOPS FOR GAS OPTIMIZATIONInformationalAcknowledged

8. Findings & Tech Details

8.1 POSSIBLE LOSS OF OWNERSHIP

// Medium

Description

When transferring ownership of the protocol, no checks are performed on whether the new address is valid and active. In case there is a mistake when transferring the ownership, the whole protocol is locked out of its permissioned functionalities.

Code Location

Ownable.sol

    function transferOwnership(address _newOwner) public virtual onlyOwner {
        require(_newOwner != address(0), "Ownable: new owner is the zero address");
        _setOwner(_newOwner);
    }
Score
Impact: 5
Likelihood: 1
Recommendation

RISK ACCEPTED: The NFTfi team accepted the risk of this finding. The team most likely will implement the remediation when they upgrade the platform.

8.2 ZERO ADDRESS NOT CHECKED

// Informational

Description

In the constructor of the DirectLoanFixedOfferRedeploy.sol contract, admin and nfthub contract address variables are not being checked to avoid pointing to the zero address, extending this issue to the parent contracts.

Code Location

DirectLoanFixedOfferRedeploy.sol

    constructor(
        address _admin,
        address _nftfiHub,
        address[] memory _permittedErc20s
    ) DirectLoanFixedOffer(_admin, _nftfiHub, _permittedErc20s) {
        // solhint-disable-previous-line no-empty-blocks
    }
Score
Impact: 1
Likelihood: 2
Recommendation

ACKNOWLEDGED: The NFTfi team acknowledged this issue.

8.3 USE I++ INSTEAD OF ++I IN LOOPS FOR GAS OPTIMIZATION

// Informational

Description

In the setERC20Permits function, within the loop, the variable i is incremented using i++. It is known that, in loops, using ++i costs less gas per iteration than i++. This also affects variables incremented inside the loop code block.

Code Location

DirectLoanBaseMinimal.sol

    function setERC20Permits(address[] memory _erc20s, bool[] memory _permits) external onlyOwner {
        require(_erc20s.length == _permits.length, "setERC20Permits function information arity mismatch");

        for (uint256 i = 0; i < _erc20s.length; i++) {
            _setERC20Permit(_erc20s[i], _permits[i]);
        }
    }
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The NFTfi team acknowledged this issue.

9. Review Notes

In the manual testing phase, the following scenarios were simulated. The scenarios listed below were selected based on the severity of the vulnerabilities Halborn was testing the program for.

SCENARIOS TESTED

  • Test 1: Lending - Borrow - normal repay procedure
  • Test 2: Lending - Borrow and repay Out Of Time (revert expected)
  • Test 3: Lending - Borrow - Out Of Time - normal liquidation procedure
  • Test 4: Lending - Borrow and liquidate ahead of schedule (revert expected)
  • Test 5: Lending - Borrow - Out of time - Renegotiate - normal repay procedure
  • Test 6: Attack: Borrower tries to renegotiate their own loan to steal the NFT (revert expected)

Script

The following test environment was set up for the purposes of executing the above scenarios:

NFTFi.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;

import "forge-std/Test.sol";
import "../src/contracts/NftfiHub.sol";
import "../src/contracts/loans/direct/loanTypes/DirectLoanFixedOfferRedeploy.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../src/contracts/mocks/NFT.sol";
import "../src/contracts/mocks/NFTWrapper.sol";
import "../src/contracts/loans/direct/DirectLoanCoordinator.sol";
import "../src/contracts/mocks/SimpleERC20.sol";
import "../src/contracts/permittedLists/PermittedNFTsAndTypeRegistry.sol";
import "../src/contracts/loans/direct/loanTypes/LoanData.sol";

contract NftfiTest is Test {
    using ECDSA for bytes32;

    NFT internal nftContract;
    NFTWrapper internal nftWrapper;
    SmartNft internal nftPromissoryNote;
    SmartNft internal nftObligationReceipt;
    SimpleToken internal token;
    NftfiHub internal nftFiHub;
    DirectLoanCoordinator internal directLoanCoordinator;
    DirectLoanFixedOfferRedeploy internal directLoanFixedOfferRedeploy;
    PermittedNFTsAndTypeRegistry internal permittedNFTsAndTypeRegistry;

    address internal owner;
    address internal admin;
    address internal alice;
    address internal bobby;
    address internal carla;
    address internal edgar;
    address internal zeroo;
    uint256 internal verifyingSignerPrivateKey;
    address internal verifyingSigner;
    address internal dappIdentifier1;
    address internal sender;
    address[] internal permittedErc20s;
    string[] internal contractKeys;
    address[] internal contractAddresses;
    string[] internal definedNftTypes;
    address[] internal definedNftWrappers;
    address[] internal permittedNftContracts;
    string[] internal permittedNftTypes;
    uint256 internal timeNow = block.timestamp;
    uint256 internal day = 86400;
    string[] internal loanTypes;
    address[] internal loanContracts;
    bool internal liquidated;



    function setUp() public {

        /* *************** */
        /* ADDRESSES SETUP */
        /* *************** */

        // ADDRESSES DECLARATION
        owner = vm.addr(0xAA);
        alice = vm.addr(0xAB);
        bobby = vm.addr(0xAC);
        carla = vm.addr(0xAD);
        edgar = vm.addr(0xAE);
        zeroo = address(0);
        admin = owner;

        // 100 ETHER PER ADDRESS
        vm.deal(owner, 100 ether);
        vm.deal(alice, 100 ether);
        vm.deal(bobby, 100 ether);
        vm.deal(carla, 100 ether);
        vm.deal(edgar, 100 ether);

        // LABELING ADDRESSES
        vm.label(owner, "owner");
        vm.label(alice, "alice");
        vm.label(bobby, "bobby");
        vm.label(carla, "carla");
        vm.label(edgar, "edgar");

        /* ***************** */
        /* ENVIRONMENT SETUP */
        /* ***************** */

        // DEPLOYING NFTWRAPPER
        vm.prank(admin);
        nftWrapper = new NFTWrapper();

        // DEPLOYING NFT CONTRACT
        vm.prank(admin);
        nftContract = new NFT(address(nftWrapper));

        // MINT 5 NFTS TO ALICE
        vm.prank(alice);
        nftContract.mintNFT(5);

        // DEPLOYING AND MINTING TOKEN
        vm.prank(admin);
        token = new SimpleToken("token", "TKN", 1000000_000000000000000000);
        vm.prank(admin);
        token.transfer(alice, 50_000000000000000000);
        vm.prank(admin);
        token.transfer(bobby, 1000_000000000000000000);
        vm.prank(admin);
        token.transfer(carla, 250_000000000000000000);

        // LOGS
        console.log("****** SETTING ENVIRONMENT ******"); 
        console.log("****** STATE 1 *******");  
        console.log("****** BALANCES ******");     
        console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
        console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
        console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
        console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
        console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
        console.log(" ");
        console.log(" ");

        // PUSHING KEYS AND ADDRESSES OF NFT AND TOKEN CONTRACTS
        contractKeys.push('PERMITTED_NFTS');
        contractAddresses.push(address(nftContract));
        permittedErc20s.push(address(token));

        // DEPLOYING NFT HUB
        nftFiHub = new NftfiHub(admin, contractKeys, contractAddresses);
        address nftFiHubAddr = address(nftFiHub);

        // DEPLOYING PROMISORY NOTES AND OBLIGATION RECEPT CONTRACTS
        nftObligationReceipt = new SmartNft(admin, address(nftFiHub), address(directLoanCoordinator), "nftObligationReceipt", "NOR", "customURI");
        nftPromissoryNote = new SmartNft(admin, address(nftFiHub), address(directLoanCoordinator), "nftPromissoryNote", "NOR", "customURI");
        vm.prank(admin);
        nftObligationReceipt.setLoanCoordinator(address(directLoanCoordinator));
        vm.prank(admin);
        nftPromissoryNote.setLoanCoordinator(address(directLoanCoordinator));

        // DEPLOYING DIRECT_LOAN_FIXED_OFFER_REDEPLOY
        directLoanFixedOfferRedeploy = new DirectLoanFixedOfferRedeploy(admin, nftFiHubAddr, permittedErc20s);
        address directLoanFixedOfferRedeployAddr = address(directLoanFixedOfferRedeploy);

        // DEPLOYING DIRECT LOAN COORDINATOR
        loanTypes.push("DIRECT_LOAN_FIXED_REDEPLOY");
        loanContracts.push(address(directLoanFixedOfferRedeploy));
        directLoanCoordinator = new DirectLoanCoordinator(address(nftFiHub), admin, loanTypes, loanContracts);

        // INITIALIZING DIRECT LOAN COORDINATOR
        directLoanCoordinator.initialize(address(nftPromissoryNote), address(nftObligationReceipt));
        vm.prank(admin);
        nftObligationReceipt.setLoanCoordinator(address(directLoanCoordinator));
        vm.prank(admin);
        nftPromissoryNote.setLoanCoordinator(address(directLoanCoordinator));

        // SETTING CONTRACTKEYS AND CONTRACTADDRRESSES
        contractKeys.push('DIRECT_LOAN_COORDINATOR');
        contractAddresses.push(address(directLoanCoordinator)); 
        vm.prank(admin);
        nftFiHub.setContract('DIRECT_LOAN_COORDINATOR', address(directLoanCoordinator));

        // REGISTRATION OF PERMITTED NFTS AND TYPES
        definedNftTypes.push('ERC721');
        definedNftWrappers.push(address(nftWrapper));
        permittedNftContracts.push(address(nftContract));
        permittedNftTypes.push('ERC721');
        permittedNFTsAndTypeRegistry = new PermittedNFTsAndTypeRegistry(admin, nftFiHubAddr, definedNftTypes, definedNftWrappers, permittedNftContracts, permittedNftTypes);

        // APPROVALS FOR PUT NFTS AS COLATERAL
        vm.prank(alice);
        nftContract.setApprovalForAll(address(directLoanFixedOfferRedeployAddr), true);
    }

TEST 1

Script

NFTFi.t.sol

function test_1() public {

    /* ****************************************** */
    /* TEST 1: LENDING BORROWING NORMAL PROCEDURE */
    /* ****************************************** */

    console.log("******************************");
    console.log("**********  TEST 1  **********"); 
    console.log("******************************"); 
    console.log(" "); 

    // DECLARING OFFER
    LoanData.Offer memory offer = LoanData.Offer({
        loanPrincipalAmount: 10_000000000000000000,
        maximumRepaymentAmount: 12_000000000000000000,
        nftCollateralId: 1,
        nftCollateralContract: address(nftContract),
        loanDuration: 10 days,
        loanAdminFeeInBasisPoints: 500,
        loanERC20Denomination: address(token),
        referrer: zeroo
    });

    // TOKEN APPROVAL
    vm.prank(bobby);
    token.approve(address(directLoanFixedOfferRedeploy), 10_000000000000000000);

    // GETTING CHAIN ID
    uint256 id;
    assembly {
        id := chainid()
    }

    // PREPARING SIGNATURE STRUCT
    LoanData.Signature memory signature = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: hex"1c"
    });

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
        abi.encodePacked(getEncodedOffer(offer), getEncodedSignature(signature), address(directLoanFixedOfferRedeploy), id)
    );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory signaturesf = bytes.concat(r, s, v_bytes);

    // BOBBY SIGNES
    LoanData.Signature memory signaturefi = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: signaturesf
    });

    // ALICE ACCEPTS BOBBY'S OFFER
    LoanData.BorrowerSettings memory borrowerSettings;
    vm.prank(alice);
    directLoanFixedOfferRedeploy.acceptOffer(offer, signaturefi, borrowerSettings);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 loanPrincipalAmount, uint256 maximumRepaymentAmount, uint256 nftCollateralId, address loanERC20Denomination, uint32 loanDuration, uint32 loanInterestRateForDuration, uint16 loanAdminFeeInBasisPoints, address nftCollateralWrapper, uint64 loanStartTime, address nftCollateralContract, address borrower) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);

    // LOGS
    console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
    console.log(" ");
    console.log("****** STATE 1 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,loanPrincipalAmount);
    console.log("maximumRepaymentAmount     ---> " ,maximumRepaymentAmount);
    console.log("nftCollateralId            ---> " ,nftCollateralId);
    console.log("loanERC20Denomination      ---> " ,loanERC20Denomination);
    console.log("loanDuration               ---> " ,loanDuration);
    console.log("interestRateForDuration    ---> " ,loanInterestRateForDuration);
    console.log("loanAdminFeeInBasisPoints  ---> " ,loanAdminFeeInBasisPoints);
    console.log("nftCollateralWrapper       ---> " ,nftCollateralWrapper);
    console.log("loanStartTime              ---> " ,loanStartTime);
    console.log("nftCollateralContract      ---> " ,nftCollateralContract);
    console.log("borrower                   ---> " ,borrower);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");

    // 5 DAYS LATER
    console.log("5 DAYS LATER...");
    vm.warp(5 * day);

    // ALICE PAY THE MONEY
    vm.prank(alice);
    token.approve(address(directLoanFixedOfferRedeploy), 12_000000000000000000);
    vm.prank(alice);
    directLoanFixedOfferRedeploy.payBackLoan(1);


    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 a, uint256 b, uint256 c, address d, uint32 e, uint32 f, uint16 g, address h, uint64 i, address j, address k) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);
    console.log("TX: ALICE ---> PAY BACK LOAN"); 
    console.log(" ");
    console.log("****** STATE 3 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,a);
    console.log("maximumRepaymentAmount     ---> " ,b);
    console.log("nftCollateralId            ---> " ,c);
    console.log("loanERC20Denomination      ---> " ,d);
    console.log("loanDuration               ---> " ,e);
    console.log("interestRateForDuration    ---> " ,f);
    console.log("loanAdminFeeInBasisPoints  ---> " ,g);
    console.log("nftCollateralWrapper       ---> " ,h);
    console.log("loanStartTime              ---> " ,i);
    console.log("nftCollateralContract      ---> " ,j);
    console.log("borrower                   ---> " ,k);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");
}

Output

TEST 2

Script

NFTFi.t.sol

function test_2() public {

    /* ****************************************************** */
    /* TEST 2: LENDING BORROWING NORMAL PROCEDURE OUT OF TIME */
    /* ****************************************************** */

    console.log("******************************");
    console.log("**********  TEST 2  **********"); 
    console.log("******************************"); 
    console.log(" "); 

    // DECLARING OFFER
    LoanData.Offer memory offer = LoanData.Offer({
        loanPrincipalAmount: 10_000000000000000000,
        maximumRepaymentAmount: 12_000000000000000000,
        nftCollateralId: 1,
        nftCollateralContract: address(nftContract),
        loanDuration: 10 days,
        loanAdminFeeInBasisPoints: 500,
        loanERC20Denomination: address(token),
        referrer: zeroo
    });

    // TOKEN APPROVAL
    vm.prank(bobby);
    token.approve(address(directLoanFixedOfferRedeploy), 10_000000000000000000);

    // GETTING CHAIN ID
    uint256 id;
    assembly {
        id := chainid()
    }

    // PREPARING SIGNATURE STRUCT
    LoanData.Signature memory signature = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: hex"1c"
    });

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
        abi.encodePacked(getEncodedOffer(offer), getEncodedSignature(signature), address(directLoanFixedOfferRedeploy), id)
    );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory signaturesf = bytes.concat(r, s, v_bytes);

    // BOBBY SIGNES
    LoanData.Signature memory signaturefi = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: signaturesf
    });

    // ALICE ACCEPTS BOBBY'S OFFER
    LoanData.BorrowerSettings memory borrowerSettings;
    vm.prank(alice);
    directLoanFixedOfferRedeploy.acceptOffer(offer, signaturefi, borrowerSettings);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 loanPrincipalAmount, uint256 maximumRepaymentAmount, uint256 nftCollateralId, address loanERC20Denomination, uint32 loanDuration, uint32 loanInterestRateForDuration, uint16 loanAdminFeeInBasisPoints, address nftCollateralWrapper, uint64 loanStartTime, address nftCollateralContract, address borrower) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);

    // LOGS
    console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
    console.log(" ");
    console.log("****** STATE 1 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,loanPrincipalAmount);
    console.log("maximumRepaymentAmount     ---> " ,maximumRepaymentAmount);
    console.log("nftCollateralId            ---> " ,nftCollateralId);
    console.log("loanERC20Denomination      ---> " ,loanERC20Denomination);
    console.log("loanDuration               ---> " ,loanDuration);
    console.log("interestRateForDuration    ---> " ,loanInterestRateForDuration);
    console.log("loanAdminFeeInBasisPoints  ---> " ,loanAdminFeeInBasisPoints);
    console.log("nftCollateralWrapper       ---> " ,nftCollateralWrapper);
    console.log("loanStartTime              ---> " ,loanStartTime);
    console.log("nftCollateralContract      ---> " ,nftCollateralContract);
    console.log("borrower                   ---> " ,borrower);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");

    // 15 DAYS LATER
    console.log("15 DAYS LATER...");
    vm.warp(15 * day);

    // ALICE PAY THE MONEY
    vm.prank(alice);
    token.approve(address(directLoanFixedOfferRedeploy), 12_000000000000000000);

    vm.prank(alice);
    vm.expectRevert();
    directLoanFixedOfferRedeploy.payBackLoan(1);


    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 a, uint256 b, uint256 c, address d, uint32 e, uint32 f, uint16 g, address h, uint64 i, address j, address k) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);
    console.log("TX: ALICE ---> PAY BACK LOAN");
    console.log(" ");
    console.log("****** STATE 3 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,a);
    console.log("maximumRepaymentAmount     ---> " ,b);
    console.log("nftCollateralId            ---> " ,c);
    console.log("loanERC20Denomination      ---> " ,d);
    console.log("loanDuration               ---> " ,e);
    console.log("interestRateForDuration    ---> " ,f);
    console.log("loanAdminFeeInBasisPoints  ---> " ,g);
    console.log("nftCollateralWrapper       ---> " ,h);
    console.log("loanStartTime              ---> " ,i);
    console.log("nftCollateralContract      ---> " ,j);
    console.log("borrower                   ---> " ,k);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");
}

Output

TEST 3

Script

NFTFi.t.sol

function test_1() public {

    /* ****************************************** */
    /* TEST 1: LENDING BORROWING NORMAL PROCEDURE */
    /* ****************************************** */

    console.log("******************************");
    console.log("**********  TEST 1  **********"); 
    console.log("******************************"); 
    console.log(" "); 

    // DECLARING OFFER
    LoanData.Offer memory offer = LoanData.Offer({
        loanPrincipalAmount: 10_000000000000000000,
        maximumRepaymentAmount: 12_000000000000000000,
        nftCollateralId: 1,
        nftCollateralContract: address(nftContract),
        loanDuration: 10 days,
        loanAdminFeeInBasisPoints: 500,
        loanERC20Denomination: address(token),
        referrer: zeroo
    });

    // TOKEN APPROVAL
    vm.prank(bobby);
    token.approve(address(directLoanFixedOfferRedeploy), 10_000000000000000000);

    // GETTING CHAIN ID
    uint256 id;
    assembly {
        id := chainid()
    }

    // PREPARING SIGNATURE STRUCT
    LoanData.Signature memory signature = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: hex"1c"
    });

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
        abi.encodePacked(getEncodedOffer(offer), getEncodedSignature(signature), address(directLoanFixedOfferRedeploy), id)
    );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory signaturesf = bytes.concat(r, s, v_bytes);

    // BOBBY SIGNES
    LoanData.Signature memory signaturefi = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: signaturesf
    });

    // ALICE ACCEPTS BOBBY'S OFFER
    LoanData.BorrowerSettings memory borrowerSettings;
    vm.prank(alice);
    directLoanFixedOfferRedeploy.acceptOffer(offer, signaturefi, borrowerSettings);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 loanPrincipalAmount, uint256 maximumRepaymentAmount, uint256 nftCollateralId, address loanERC20Denomination, uint32 loanDuration, uint32 loanInterestRateForDuration, uint16 loanAdminFeeInBasisPoints, address nftCollateralWrapper, uint64 loanStartTime, address nftCollateralContract, address borrower) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);

    // LOGS
    console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
    console.log(" ");
    console.log("****** STATE 1 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,loanPrincipalAmount);
    console.log("maximumRepaymentAmount     ---> " ,maximumRepaymentAmount);
    console.log("nftCollateralId            ---> " ,nftCollateralId);
    console.log("loanERC20Denomination      ---> " ,loanERC20Denomination);
    console.log("loanDuration               ---> " ,loanDuration);
    console.log("interestRateForDuration    ---> " ,loanInterestRateForDuration);
    console.log("loanAdminFeeInBasisPoints  ---> " ,loanAdminFeeInBasisPoints);
    console.log("nftCollateralWrapper       ---> " ,nftCollateralWrapper);
    console.log("loanStartTime              ---> " ,loanStartTime);
    console.log("nftCollateralContract      ---> " ,nftCollateralContract);
    console.log("borrower                   ---> " ,borrower);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");

    // 5 DAYS LATER
    console.log("5 DAYS LATER...");
    vm.warp(5 * day);

    // ALICE PAY THE MONEY
    vm.prank(alice);
    token.approve(address(directLoanFixedOfferRedeploy), 12_000000000000000000);
    vm.prank(alice);
    directLoanFixedOfferRedeploy.payBackLoan(1);


    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 a, uint256 b, uint256 c, address d, uint32 e, uint32 f, uint16 g, address h, uint64 i, address j, address k) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);
    console.log("TX: ALICE ---> PAY BACK LOAN"); 
    console.log(" ");
    console.log("****** STATE 3 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,a);
    console.log("maximumRepaymentAmount     ---> " ,b);
    console.log("nftCollateralId            ---> " ,c);
    console.log("loanERC20Denomination      ---> " ,d);
    console.log("loanDuration               ---> " ,e);
    console.log("interestRateForDuration    ---> " ,f);
    console.log("loanAdminFeeInBasisPoints  ---> " ,g);
    console.log("nftCollateralWrapper       ---> " ,h);
    console.log("loanStartTime              ---> " ,i);
    console.log("nftCollateralContract      ---> " ,j);
    console.log("borrower                   ---> " ,k);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");
}

Output

TEST 4

Script

NFTFi.t.sol

    function test_4() public {

    /* ***************************************************** */
    /* TEST 4: LENDING + LIQUIDATION BEFORE THAN LOAN FINISH */
    /* ***************************************************** */

    console.log("******************************");
    console.log("**********  TEST 4  **********"); 
    console.log("******************************"); 
    console.log(" "); 

    // DECLARING OFFER
    LoanData.Offer memory offer = LoanData.Offer({
        loanPrincipalAmount: 10_000000000000000000,
        maximumRepaymentAmount: 12_000000000000000000,
        nftCollateralId: 1,
        nftCollateralContract: address(nftContract),
        loanDuration: 10 days,
        loanAdminFeeInBasisPoints: 500,
        loanERC20Denomination: address(token),
        referrer: zeroo
    });

    // TOKEN APPROVAL
    vm.prank(bobby);
    token.approve(address(directLoanFixedOfferRedeploy), 10_000000000000000000);

    // GETTING CHAIN ID
    uint256 id;
    assembly {
        id := chainid()
    }

    // PREPARING SIGNATURE STRUCT
    LoanData.Signature memory signature = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: hex"1c"
    });

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
        abi.encodePacked(getEncodedOffer(offer), getEncodedSignature(signature), address(directLoanFixedOfferRedeploy), id)
    );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory signaturesf = bytes.concat(r, s, v_bytes);

    // BOBBY SIGNES
    LoanData.Signature memory signaturefi = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: signaturesf
    });

    // ALICE ACCEPTS BOBBY'S OFFER
    LoanData.BorrowerSettings memory borrowerSettings;
    vm.prank(alice);
    directLoanFixedOfferRedeploy.acceptOffer(offer, signaturefi, borrowerSettings);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 loanPrincipalAmount, uint256 maximumRepaymentAmount, uint256 nftCollateralId, address loanERC20Denomination, uint32 loanDuration, uint32 loanInterestRateForDuration, uint16 loanAdminFeeInBasisPoints, address nftCollateralWrapper, uint64 loanStartTime, address nftCollateralContract, address borrower) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);

    // LOGS
    console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
    console.log(" ");
    console.log("****** STATE 1 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,loanPrincipalAmount);
    console.log("maximumRepaymentAmount     ---> " ,maximumRepaymentAmount);
    console.log("nftCollateralId            ---> " ,nftCollateralId);
    console.log("loanERC20Denomination      ---> " ,loanERC20Denomination);
    console.log("loanDuration               ---> " ,loanDuration);
    console.log("interestRateForDuration    ---> " ,loanInterestRateForDuration);
    console.log("loanAdminFeeInBasisPoints  ---> " ,loanAdminFeeInBasisPoints);
    console.log("nftCollateralWrapper       ---> " ,nftCollateralWrapper);
    console.log("loanStartTime              ---> " ,loanStartTime);
    console.log("nftCollateralContract      ---> " ,nftCollateralContract);
    console.log("borrower                   ---> " ,borrower);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");

    // 3 DAYS LATER
    vm.warp(3 * day);
    console.log("3 DAYS LATER...");

    // BOBBY LIQUIDATES THE LOAN
    vm.prank(bobby);
    vm.expectRevert();
    directLoanFixedOfferRedeploy.liquidateOverdueLoan(1);


    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 a, uint256 b, uint256 c, address d, uint32 e, uint32 f, uint16 g, address h, uint64 i, address j, address k) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);
    console.log("TX: BOBBY ---> LIQUIDATE LOAN");
    console.log(" ");
    console.log("****** STATE 3 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,a);
    console.log("maximumRepaymentAmount     ---> " ,b);
    console.log("nftCollateralId            ---> " ,c);
    console.log("loanERC20Denomination      ---> " ,d);
    console.log("loanDuration               ---> " ,e);
    console.log("interestRateForDuration    ---> " ,f);
    console.log("loanAdminFeeInBasisPoints  ---> " ,g);
    console.log("nftCollateralWrapper       ---> " ,h);
    console.log("loanStartTime              ---> " ,i);
    console.log("nftCollateralContract      ---> " ,j);
    console.log("borrower                   ---> " ,k);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");
}

function firstStep_test5() internal {

    // DECLARING OFFER
    LoanData.Offer memory offer = LoanData.Offer({
        loanPrincipalAmount: 10_000000000000000000,
        maximumRepaymentAmount: 12_000000000000000000,
        nftCollateralId: 1,
        nftCollateralContract: address(nftContract),
        loanDuration: 10 days,
        loanAdminFeeInBasisPoints: 500,
        loanERC20Denomination: address(token),
        referrer: zeroo
    });

    // TOKEN APPROVAL
    vm.prank(bobby);
    token.approve(address(directLoanFixedOfferRedeploy), 10_000000000000000000);

    // GETTING CHAIN ID
    uint256 id;
    assembly {
        id := chainid()
    }

    // PREPARING SIGNATURE STRUCT
    LoanData.Signature memory signature = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: hex"1c"
    });

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
        abi.encodePacked(getEncodedOffer(offer), getEncodedSignature(signature), address(directLoanFixedOfferRedeploy), id)
    );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory signaturesf = bytes.concat(r, s, v_bytes);

    // BOBBY SIGNES
    LoanData.Signature memory signaturefi = LoanData.Signature({
        nonce: 1,
        expiry: timeNow + 10 days,
        signer: bobby,
        signature: signaturesf
    });

    // ALICE ACCEPTS BOBBY'S OFFER
    LoanData.BorrowerSettings memory borrowerSettings;
    vm.prank(alice);
    directLoanFixedOfferRedeploy.acceptOffer(offer, signaturefi, borrowerSettings);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 loanPrincipalAmount, uint256 maximumRepaymentAmount, uint256 nftCollateralId, address loanERC20Denomination, uint32 loanDuration, uint32 loanInterestRateForDuration, uint16 loanAdminFeeInBasisPoints, address nftCollateralWrapper, uint64 loanStartTime, address nftCollateralContract, address borrower) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);

    // LOGS
    console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
    console.log(" ");
    console.log("****** STATE 1 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,loanPrincipalAmount);
    console.log("maximumRepaymentAmount     ---> " ,maximumRepaymentAmount);
    console.log("nftCollateralId            ---> " ,nftCollateralId);
    console.log("loanERC20Denomination      ---> " ,loanERC20Denomination);
    console.log("loanDuration               ---> " ,loanDuration);
    console.log("interestRateForDuration    ---> " ,loanInterestRateForDuration);
    console.log("loanAdminFeeInBasisPoints  ---> " ,loanAdminFeeInBasisPoints);
    console.log("nftCollateralWrapper       ---> " ,nftCollateralWrapper);
    console.log("loanStartTime              ---> " ,loanStartTime);
    console.log("nftCollateralContract      ---> " ,nftCollateralContract);
    console.log("borrower                   ---> " ,borrower);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");
}

Output

TEST 5

Script

NFTFi.t.sol

function test_5() public {
    /* ******************************************************* */
    /* TEST 5: BORROWING + WANT TO PAY LATE + RENEGOTIATE LOAN */
    /* ******************************************************* */

    console.log("******************************");
    console.log("**********  TEST 5  **********"); 
    console.log("******************************"); 
    console.log(" "); 

    firstStep_test5();

    // 15 DAYS LATER
    console.log("12 DAYS LATER...");
    vm.warp(12 days);

    uint256 id2;
    assembly {
        id2 := chainid()
    }

    uint256 _expiry = block.timestamp + 20 days;

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
            abi.encodePacked(
                uint256(1),
                uint32(30 days),
                uint256(20_000000000000000000),
                uint256(5_000000000000000000),
                abi.encodePacked(bobby, uint256(2), _expiry),
                address(directLoanFixedOfferRedeploy),
                id2
            )
        );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory _lenderSignature = bytes.concat(r, s, v_bytes);

    // ALICE WANTS TO RENEGOTIATE
    vm.prank(alice);
    token.approve(address(directLoanFixedOfferRedeploy), 5_000000000000000000);
    vm.prank(alice);
    directLoanFixedOfferRedeploy.renegotiateLoan(1, 30 days, 20_000000000000000000, 5_000000000000000000, 2, _expiry, _lenderSignature);

    // 10 DAYS LATER
    console.log("10 DAYS LATER...");
    vm.warp(10 * day);

    // ALICE PAY THE MONEY
    vm.prank(alice);
    token.approve(address(directLoanFixedOfferRedeploy), 20_000000000000000000);
    vm.prank(alice);
    token.approve(address(directLoanFixedOfferRedeploy), 20_000000000000000000);
    vm.prank(alice);
    directLoanFixedOfferRedeploy.payBackLoan(1);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 a, uint256 b, uint256 c, address d, uint32 e, uint32 f, uint16 g, address h, uint64 i, address j, address k) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);
    console.log("TX: ALICE ---> PAY BACK LOAN");
    console.log(" ");
    console.log("****** STATE 3 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,a);
    console.log("maximumRepaymentAmount     ---> " ,b);
    console.log("nftCollateralId            ---> " ,c);
    console.log("loanERC20Denomination      ---> " ,d);
    console.log("loanDuration               ---> " ,e);
    console.log("interestRateForDuration    ---> " ,f);
    console.log("loanAdminFeeInBasisPoints  ---> " ,g);
    console.log("nftCollateralWrapper       ---> " ,h);
    console.log("loanStartTime              ---> " ,i);
    console.log("nftCollateralContract      ---> " ,j);
    console.log("borrower                   ---> " ,k);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");  
}

Output

TEST 6

Script

NFTFi.t.sol

function test_6() public {
    /* ****************************************************************** */
    /* TEST 6: ATTACK LENDER WANTS TO STEAL NFT BY RENEGOTIATE LOAN EARLY */
    /* ****************************************************************** */

    console.log("******************************");
    console.log("**********  TEST 6  **********"); 
    console.log("******************************"); 
    console.log(" "); 

    firstStep_test5();

    // 1 DAY LATER
    console.log("1 DAYS LATER...");
    vm.warp(1 days);

    // MSG HASH PREPARATION
    uint256 id2;
    assembly {
        id2 := chainid()
    }
    uint256 _expiry = block.timestamp + 10; // + 10 seconds

    // GETTING THE MESSAGE HASH
    bytes32 message = keccak256(
            abi.encodePacked(
                uint256(1),
                uint32(0),
                uint256(1_000000000000000000),
                uint256(0),
                abi.encodePacked(bobby, uint256(2), _expiry),
                address(directLoanFixedOfferRedeploy),
                id2
            )
        );

    // EIP712 STANDARD
    bytes32 signedMessage = ECDSA.toEthSignedMessageHash(message);

    // GETTING THE V, R, S OF THE SIGNED MESSAGE
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xAC, signedMessage);
    bytes memory v_bytes;
    if(v == 27){v_bytes = hex"1b";} else {v_bytes = hex"1c";}
    bytes memory _lenderSignature = bytes.concat(r, s, v_bytes);

    // ALICE WANTS TO RENEGOTIATE
    vm.prank(bobby);
    vm.expectRevert();
    directLoanFixedOfferRedeploy.renegotiateLoan(1, 0, 1_000000000000000000, 0, 2, _expiry, _lenderSignature);

    // BOBBY TRIES TO LIQUIDATE THE LOAN
    vm.prank(bobby);
    vm.expectRevert();
    directLoanFixedOfferRedeploy.liquidateOverdueLoan(1);

    // CHECKING THAT THE STATE IS AS EXPECTED
    (uint256 a, uint256 b, uint256 c, address d, uint32 e, uint32 f, uint16 g, address h, uint64 i, address j, address k) = directLoanFixedOfferRedeploy.loanIdToLoan(1);
    liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(1);
    console.log("TX: ALICE ---> PAY BACK LOAN");
    console.log(" ");
    console.log("****** STATE 3 *******");  
    console.log("****** BALANCES ******");     
    console.log("Balance Of Admin        ---> " ,token.balanceOf(admin));
    console.log("Balance Of Alice        ---> " ,token.balanceOf(alice));
    console.log("Balance Of Bobby        ---> " ,token.balanceOf(bobby));
    console.log("Balance Of Carla        ---> " ,token.balanceOf(carla));
    console.log("Owner of the NFT        ---> " ,nftContract.ownerOf(1));
    console.log(" ");
    console.log("****** LOAN DATA ******");       
    console.log("loanPrincipalAmount        ---> " ,a);
    console.log("maximumRepaymentAmount     ---> " ,b);
    console.log("nftCollateralId            ---> " ,c);
    console.log("loanERC20Denomination      ---> " ,d);
    console.log("loanDuration               ---> " ,e);
    console.log("interestRateForDuration    ---> " ,f);
    console.log("loanAdminFeeInBasisPoints  ---> " ,g);
    console.log("nftCollateralWrapper       ---> " ,h);
    console.log("loanStartTime              ---> " ,i);
    console.log("nftCollateralContract      ---> " ,j);
    console.log("borrower                   ---> " ,k);
    console.log("LOAN LIQUIDATED / REPAYED  ---> " ,liquidated);
    console.log(" ");
}

Output

10. Automated Testing

STATIC ANALYSIS REPORT

Description

Halborn used automated testing techniques to enhance the coverage of certain areas of the scoped contracts. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified all the contracts in the repository and was able to compile them correctly into their ABI and binary formats, Slither was run on the all-scoped contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.

Slither results

DirectLoanFixedOfferRedeploy.sol

  • As a result of the tests carried out with the Slither tool, some results were obtained and reviewed by Halborn. Based on the results reviewed, some vulnerabilities were determined to be false positives.

AUTOMATED SECURITY SCAN

Description

Halborn used automated security scanners to assist with detection of well-known security issues, and to identify low-hanging fruits on the targets for this engagement. Among the tools used was MythX, a security analysis service for Ethereum smart contracts. MythX performed a scan on all the contracts and sent the compiled results to the analyzers to locate any vulnerabilities.

MythX results

DirectLoanFixedOfferRedeploy.sol

  • No major issues found by Mythx. The reentrancy issue flagged by MythX is a false positive as the function is already protected against reentrancy attacks by using the nonreentrant modifer.

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.