Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: October 31st, 2022 - November 7th, 2022
100% of all REPORTED Findings have been addressed
All findings
3
Critical
0
High
0
Medium
1
Low
0
Informational
2
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.
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.
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
)
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
Critical
0
High
0
Medium
1
Low
0
Informational
2
Impact x Likelihood
HAL-01
HAL-03
HAL-02
Security analysis | Risk level | Remediation Date |
---|---|---|
POSSIBLE LOSS OF OWNERSHIP | Medium | Risk Accepted |
ZERO ADDRESS NOT CHECKED | Informational | Acknowledged |
USE I++ INSTEAD OF ++I IN LOOPS FOR GAS OPTIMIZATION | Informational | Acknowledged |
// Medium
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.
function transferOwnership(address _newOwner) public virtual onlyOwner {
require(_newOwner != address(0), "Ownable: new owner is the zero address");
_setOwner(_newOwner);
}
RISK ACCEPTED: The NFTfi team
accepted the risk of this finding. The team most likely will implement the remediation when they upgrade the platform.
// Informational
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.
constructor(
address _admin,
address _nftfiHub,
address[] memory _permittedErc20s
) DirectLoanFixedOffer(_admin, _nftfiHub, _permittedErc20s) {
// solhint-disable-previous-line no-empty-blocks
}
ACKNOWLEDGED: The NFTfi team
acknowledged this issue.
// Informational
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.
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]);
}
}
ACKNOWLEDGED: The NFTfi team
acknowledged this issue.
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.
The following test environment was set up for the purposes of executing the above scenarios:
// 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);
}
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(" ");
}
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(" ");
}
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(" ");
}
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(" ");
}
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(" ");
}
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(" ");
}
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.
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.
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.
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed