Halborn Logo

icon

Native Punk Wrapper - NFTfi


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: January 17th, 2023 - February 10th, 2023

Summary

0% of all REPORTED Findings have been addressed

All findings

1

Critical

0

High

0

Medium

0

Low

0

Informational

1


1. INTRODUCTION

Native Punk Wrapper introduces support for Native Punk NFTs as collateral in the NFTfi protocol.

NFTfi engaged Halborn to conduct a security audit on their smart contracts beginning on 2023-01-17 and ending on 2023-02-10. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn was provided three weeks 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 a security risk that was 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:

    • PunkWrapper.sol

Commit ID:

Fixed commit ID:

5. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

5.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILIY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

5.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

5.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

6. SCOPE

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

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

0

Informational

1

Security analysisRisk levelRemediation Date
AIRDROP RECEIVER FUNCTIONALITY DENIEDInformational-

8. Findings & Tech Details

8.1 AIRDROP RECEIVER FUNCTIONALITY DENIED

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

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: General repayment cycle (Borrower - Escrow - Borrower)
  • Test 1.1: Attack vector: External user repays loan to steal the NFT (revert expected)
  • Test 2: General repayment cycle out of time (revert expected)
  • Test 3: General liquidation cycle (Borrower - Escrow - Lender)
  • Test 4: General liquidation cycle before agreement (revert expected)
  • Test 5: General repayment cycle out of time with renegotiation
  • Test 6: Attack vector: Borrower renegotiate their loan to steal the Punk (revert expected)
  • Test 7: General repayment cycle with promissory note transfer
  • Test 8: General repayment cycle with obligation receipt transfer
  • Test 8.2: Attack vector: Obligation receipt transfer and old owner of OR repay loan.
  • Test 9: General airdrop functionality (Airdrop receiver - Wrap - Unwrap)

Script \newline

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

PunkTest.t.sol

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

import "forge-std/Test.sol";
import "../src/NftfiHub.sol";
import "../src/loans/direct/loanTypes/DirectLoanFixedOfferRedeploy.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../src/mocks/NFT.sol";
import "../src/mocks/NFTWrapper.sol";
import "../src/test/DummyPunks.sol";
import "../src/nftTypeRegistry/nftTypes/PunkWrapper.sol";
import "../src/loans/direct/DirectLoanCoordinator.sol";
import "../src/mocks/SimpleERC20.sol";
import "../src/permittedLists/PermittedAirdrops.sol";
import "../src/permittedLists/PermittedNFTsAndTypeRegistry.sol";
import "../src/loans/direct/loanTypes/LoanData.sol";
import "../src/airdrop/AirdropReceiverFactory.sol";

contract punkTest is Test {
    using ECDSA for bytes32;

    NFTWrapper internal nftWrapper;
    PunkWrapper internal punkWrapper;
    NFT internal nftContract;
    DummyPunks internal punkContract;
    SimpleToken internal token;
    SmartNft internal nftPromissoryNote;
    SmartNft internal nftObligationReceipt;
    NftfiHub internal nftFiHub;
    DirectLoanCoordinator internal directLoanCoordinator;
    DirectLoanFixedOfferRedeploy internal directLoanFixedOfferRedeploy;
    PermittedNFTsAndTypeRegistry internal permittedNFTsAndTypeRegistry;
    PermittedAirdrops internal permittedAirdrop;
    AirdropReceiver internal airdropReceiverToClone;
    AirdropReceiver internal airdropReceiver;
    AirdropReceiverFactory internal airdropReceiverFactory;

    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 sender;
    bytes4[] internal selectors;
    address[] internal airdopContracts;
    address[] internal permittedErc20s;
    string[] internal contractKeys;
    address[] internal contractAddresses;
    string[] internal definedNftTypes;
    address[] internal definedNftWrappers;
    address[] internal permittedNftContracts;
    string[] internal permittedNftTypes;
    string[] internal loanTypes;
    address[] internal loanContracts;
    bool internal liquidated;
    uint256 internal timeNow;

    function setUp() public {
        /* *************** */
        /* ADDRESSES SETUP */
        /* *************** */

        // ADDRESSES DECLARATION
        admin = vm.addr(0x60DDD);
        alice = vm.addr(0xA71CE);
        bobby = vm.addr(0xB0BB1);
        carla = vm.addr(0xCA47A);
        edgar = vm.addr(0xED6A4);
        zeroo = address(0);

        // 100 ETHER PER ADDRESS
        vm.deal(admin, 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(admin, "admin");
        vm.label(alice, "alice");
        vm.label(bobby, "bobby");
        vm.label(carla, "carla");
        vm.label(edgar, "edgar");

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

        // DEPLYING NFTWRAPPER
        vm.prank(admin);
        punkWrapper = new PunkWrapper();
        vm.prank(admin);
        nftWrapper = new NFTWrapper();

        // DEPLOYING NFT CONTRACT
        nftContract = new NFT(address(nftWrapper));
        punkContract = new DummyPunks{value: 30000000 gwei}(address(punkWrapper));

        // MINT A PUNK TO ALICE
        vm.prank(alice);
        punkContract.mintPunk(alice, 0);
        vm.prank(carla);
        nftContract.mintNFT(10);

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

        contractKeys.push('PERMITTED_NFTS');
        contractKeys.push('PERMITTED_NFTS');
        contractAddresses.push(address(nftContract));
        contractAddresses.push(address(punkContract));
        permittedErc20s.push(address(token));

        // DEPLOYING NFTFI 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");
        nftObligationReceipt.setLoanCoordinator(address(directLoanCoordinator));
        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));
        nftObligationReceipt.setLoanCoordinator(address(directLoanCoordinator));
        nftPromissoryNote.setLoanCoordinator(address(directLoanCoordinator));

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

        // REGISTRATION OF PERMITTED NFTS AND TYPES
        definedNftTypes.push('NFT');
        definedNftTypes.push('PUNKS');
        definedNftWrappers.push(address(nftWrapper));
        definedNftWrappers.push(address(punkWrapper));
        permittedNftContracts.push(address(nftContract));
        permittedNftContracts.push(address(punkContract));
        permittedNftTypes.push('NFT');
        permittedNftTypes.push('PUNKS');
        permittedNFTsAndTypeRegistry = new PermittedNFTsAndTypeRegistry(admin, nftFiHubAddr, definedNftTypes, definedNftWrappers, permittedNftContracts, permittedNftTypes);
        vm.stopPrank();

        // PERMITTED AIRDROPS SETTINGS
        selectors.push(bytes4(keccak256(bytes("mintNFT(uint256)"))));
        airdopContracts.push(address(nftContract));
        permittedAirdrop = new PermittedAirdrops(admin, airdopContracts, selectors);

        // AIRDROP RECEIVER (alt 2)
        vm.startPrank(admin);
        airdropReceiverFactory = new AirdropReceiverFactory(address(admin), address(nftFiHub));
        airdropReceiverToClone = new AirdropReceiver(address(nftFiHub));
        permittedNFTsAndTypeRegistry.setNftType('AirdropWrapper', address(airdropReceiverToClone));

        // SETTING CONTRACTS AND PERMISIONS
        permittedNftContracts.push(address(airdropReceiverToClone));
        permittedNftTypes.push('AIRDROP_RECEIVER');
        nftFiHub.setContract('AIRDROP_RECEIVER', address(airdropReceiverToClone));
        nftFiHub.setContract('AIRDROP_FACTORY', address(airdropReceiverFactory));
        nftFiHub.setContract('PERMITTED_NFTS', address(permittedNFTsAndTypeRegistry));
        nftFiHub.setContract('PERMITTED_AIRDROPS', address(permittedAirdrop));

        // time update
        timeNow = block.timestamp;
        vm.stopPrank();
    }

The following internal functions have been used for the complete execution of the tests:

PunkTest.t.sol

    function getBobbyOfferSignature() internal returns (LoanData.Signature memory) {

        // DECLARING OFFER
        LoanData.Offer memory offer = declareOffer();

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

        // PREPARING SIGNATURE STRUCT
        LoanData.Signature memory signature = LoanData.Signature({
            nonce: 0,
            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(0xB0BB1, 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: 0,
            expiry: timeNow + 10 days,
            signer: bobby,
            signature: signaturesf
        });

        return signaturefi;
    }

    function declareOffer() internal returns (LoanData.Offer memory) {

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

        return offer;
    }

    function getBalances(uint32 loanId) internal { 
        (uint256 loanPrincipalAmount, uint256 maximumRepaymentAmount, uint256 nftCollateralId, address loanERC20Denomination, uint32 loanDuration, uint32 loanInterestRateForDuration, uint16 loanAdminFeeInBasisPoints, address nftCollateralWrapper, uint64 loanStartTime, address nftCollateralContract, address borrower) = directLoanFixedOfferRedeploy.loanIdToLoan(loanId);
        liquidated = directLoanFixedOfferRedeploy.loanRepaidOrLiquidated(loanId); 

        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        ---> " ,punkContract.punkIndexToAddress(0));
        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(" ");
    }

    function getEncodedSignature(LoanData.Signature memory _signature) internal pure returns (bytes memory) {
        return abi.encodePacked(_signature.signer, _signature.nonce, _signature.expiry);
    }

    function getEncodedOffer(LoanData.Offer memory _offer) internal pure returns (bytes memory) {
        return
            abi.encodePacked(
                _offer.loanERC20Denomination,
                _offer.loanPrincipalAmount,
                _offer.maximumRepaymentAmount,
                _offer.nftCollateralContract,
                _offer.nftCollateralId,
                _offer.referrer,
                _offer.loanDuration,
                _offer.loanAdminFeeInBasisPoints
            );
    }
}

TEST 1

Script

NFTFi.t.sol

    function test_1() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
        console.log(" ");
        getBalances(1);

        // 5 DAYS LATER
        console.log("---------------");
        console.log("5 DAYS LATER...");
        console.log("---------------");
        console.log(" ");
        vm.warp(5 days);

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

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: ALICE ---> PAY BACK LOAN"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 1.1

Script

function test_13() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
        console.log(" ");
        getBalances(1);

        // 5 DAYS LATER
        console.log("---------------");
        console.log("5 DAYS LATER...");
        console.log("---------------");
        vm.warp(5 days);

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

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: CARLA ---> PAY BACK LOAN"); 
        console.log(" ");
        getBalances(1);

        punkContract.punkIndexToAddress(0);
    }

Output

TEST 2

Script

function test_2() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
        console.log(" ");
        getBalances(1);

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

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

        vm.prank(alice);
        vm.expectRevert("Loan is expired");
        directLoanFixedOfferRedeploy.payBackLoan(1);

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: ALICE ---> TRIES TO REPAY"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 3

Script

function test_3() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy)); 

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // 11 DAYS LATER
        vm.warp(11 days);
        console.log("----------------");
        console.log("11 DAYS LATER...");
        console.log("----------------");

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

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: BOBBY ---> LIQUIDATE LOAN"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 4

Script

function test_4() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // 3 DAYS LATER
        vm.warp(3 days);
        console.log("---------------");
        console.log("3 DAYS LATER...");
        console.log("---------------");

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: BOBBY ---> TRIES TO LIQUIDATE LOAN TOO EARLY"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 5

Script

function test_5() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // 12 DAYS LATER
        console.log("----------------");
        console.log("12 DAYS LATER...");
        console.log("----------------");
        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(1), _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(0xB0BB1, 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, 1, _expiry, _lenderSignature);

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: ALICE ---> RENEGOTIATE LOAN"); 
        console.log(" ");
        getBalances(1);

        // 10 DAYS LATER
        console.log("----------------");
        console.log("10 DAYS LATER...");
        console.log("----------------");
        vm.warp(10 days);

        // 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);

        // LOGS
        console.log("****** STATE 3 *******");
        console.log("TX: ALICE ---> PAY BACK LOAN"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 6

Script

function test_6_CannotRenegotiateHisOwnLoan() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // 1 DAY LATER
        console.log("---------------");
        console.log("1 DAYS LATER...");
        console.log("---------------");
        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(
                uint32(1),
                uint32(0),
                uint256(1_000000000000000000),
                uint256(0),
                abi.encodePacked(bobby, uint256(1), _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(0xB0BB1, 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("Only borrower can initiate");
        directLoanFixedOfferRedeploy.renegotiateLoan(1, 0, 1_000000000000000000, 0, 1, _expiry, _lenderSignature);

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

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: BOBBY ---> TRIES TO RENEGOTIATE HIS OWN LOAN AND REDUCE TIME TO 0 (revert)"); 
        console.log("TX: BOBBY ---> LIQUIDATE LOAN (revert)"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 7

Script

function test_7_PromisoryNoteExchange() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // TRNSFER PROMISSORY_NOTE
        nftObligationReceipt.balanceOf(address(alice));
        nftPromissoryNote.ownerOf(5929418158485165765);
        vm.startPrank(bobby);
        nftPromissoryNote.approve(address(carla), 5929418158485165765);
        nftPromissoryNote.transferFrom(address(bobby), address(carla), 5929418158485165765);
        vm.stopPrank();

        // 5 DAYS LATER
        console.log("---------------");
        console.log("5 DAYS LATER...");
        console.log("---------------");
        vm.warp(5 days);

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

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: BOBBY ---> TRANSFER PROMISSORY NOTE TO CARLA"); 
        console.log("TX: ALICE ---> PAY BACK LOAN"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 8

Script

function test_8_ObligationReceiptExchange() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));


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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // MINT AND TRNSFER PROMISSORY_NOTE
        nftObligationReceipt.balanceOf(address(alice));
        vm.startPrank(alice);
        directLoanFixedOfferRedeploy.mintObligationReceipt(1);
        nftObligationReceipt.balanceOf(address(alice));
        nftObligationReceipt.approve(address(carla), 5929418158485165765);
        nftObligationReceipt.transferFrom(address(alice), address(carla), 5929418158485165765);
        vm.stopPrank();

        // ALICE TRY TO STEAL NFT
        vm.startPrank(carla);
        token.approve(address(directLoanFixedOfferRedeploy), 12_000000000000000000);
        directLoanFixedOfferRedeploy.payBackLoan(1);
        vm.stopPrank();

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: ALICE ---> TRANSFER OBLIGATION RECEIPT TO CARLA"); 
        console.log("TX: CARLA ---> PAY BACK LOAN"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 8.1

Script

function test_82_ObligationReceiptExchangeAttack() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));


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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

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

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT OFFER"); 
        console.log(" ");
        getBalances(1);

        // MINT AND TRNSFER PROMISSORY_NOTE
        nftObligationReceipt.balanceOf(address(alice));
        vm.startPrank(alice);
        directLoanFixedOfferRedeploy.mintObligationReceipt(1);
        nftObligationReceipt.balanceOf(address(alice));
        nftObligationReceipt.approve(address(carla), 5929418158485165765);
        nftObligationReceipt.transferFrom(address(alice), address(carla), 5929418158485165765);

        // ALICE TRY TO STEAL NFT
        token.approve(address(directLoanFixedOfferRedeploy), 12_000000000000000000);
        directLoanFixedOfferRedeploy.payBackLoan(1);
        vm.stopPrank();

        // LOGS
        console.log("****** STATE 2 *******");
        console.log("TX: ALICE ---> TRANSFER OBLIGATION RECEIPT TO CARLA"); 
        console.log("TX: ALICE ---> PAY BACK LOAN"); 
        console.log(" ");
        getBalances(1);
    }

Output

TEST 9

Script

function test_9_airdrop() public {
        function test_9_airdrop() public {

        // INITIAL STATE LOGS
        console.log("****** STATE 0 (ENV) *******");
        console.log(" ");
        getBalances();

        // AIRDROP RECEIVER (alt 2)
        vm.startPrank(admin);
        airdropReceiverFactory = new AirdropReceiverFactory(address(admin), address(nftFiHub));
        airdropReceiverToClone = new AirdropReceiver(address(nftFiHub));
        permittedNFTsAndTypeRegistry.setNftType('AirdropWrapper', address(airdropReceiverToClone));

        // SETTING CONTRACTS AND PERMISIONS
        permittedNftContracts.push(address(airdropReceiverToClone));
        permittedNftTypes.push('AIRDROP_RECEIVER');
        nftFiHub.setContract('AIRDROP_RECEIVER', address(airdropReceiverToClone));
        nftFiHub.setContract('AIRDROP_FACTORY', address(airdropReceiverFactory));
        nftFiHub.setContract('PERMITTED_NFTS', address(permittedNFTsAndTypeRegistry));
        nftFiHub.setContract('PERMITTED_AIRDROPS', address(permittedAirdrop));

        vm.stopPrank();
        vm.startPrank(alice);
        uint256 aliceWrapperId;
        address aliceAirdropReceiverAddr;
        console.logBytes(bytes(ContractKeys.AIRDROP_WRAPPER_STRING));
        vm.stopPrank();

        // -------------------------------------------
        // ALICE BORROWS GIVING PUNK AS COLLATERAL
        // -------------------------------------------

        vm.prank(alice);
        punkContract.offerPunkForSaleToAddress(0, 0, address(directLoanFixedOfferRedeploy));

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

        // PREPARING SINGNATURE
        LoanData.Offer memory offer = declareOffer();
        LoanData.Signature memory signaturefi = getBobbyOfferSignature();

        console.log("0xx", punkContract.punkIndexToAddress(0));
        // ALICE ACCEPTS BOBBY'S OFFER
        LoanData.BorrowerSettings memory borrowerSettings;
        vm.prank(alice);
        directLoanFixedOfferRedeploy.acceptOffer(offer, signaturefi, borrowerSettings);
        console.log("0yy", punkContract.punkIndexToAddress(0));

        // LOGS
        console.log("****** STATE 1 *******");
        console.log("TX: ALICE ---> ACCEPT BOBBY'S OFFER"); 
        console.log(" ");
        getBalances(1);

        // 5 DAYS LATER
        console.log("---------------");
        console.log("5 DAYS LATER...");
        console.log("---------------");
        console.log(" ");
        vm.warp(5 days);
        vm.prank(alice);
        directLoanFixedOfferRedeploy.wrapCollateral(1);

        // ALICE PULLS THE AIRDROP
        bytes memory encodedFunctionData = abi.encodeWithSignature("mintNFT(uint256)", 1);
        vm.prank(alice);
        AirdropReceiver(address(0x6d69556Cf844F68065f814F1E9E00854dDf91A28)).pullAirdrop(address(nftContract), encodedFunctionData);

        // THE LOAN IS PAID BY ALICE
        vm.startPrank(alice);
        token.approve(address(directLoanFixedOfferRedeploy), 12_000000000000000000);
        directLoanFixedOfferRedeploy.payBackLoan(1);
        AirdropReceiver(address(0x6d69556Cf844F68065f814F1E9E00854dDf91A28)).unwrap(alice);
        AirdropReceiver(address(0x6d69556Cf844F68065f814F1E9E00854dDf91A28)).drainERC721Airdrop(address(nftContract), 10, alice);

        // LOGS
        vm.stopPrank();
        console.log("ownerOfNFT", nftContract.ownerOf(10));
        console.log("0zz", punkContract.punkIndexToAddress(0));
        console.log("alice", alice);
    }

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 modifier.

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.