Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: January 17th, 2023 - February 10th, 2023
0% of all REPORTED Findings have been addressed
All findings
1
Critical
0
High
0
Medium
0
Low
0
Informational
1
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.
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.
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:
PunkWrapper.sol
Commit ID:
Fixed commit ID:
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
0
Informational
1
Security analysis | Risk level | Remediation Date |
---|---|---|
AIRDROP RECEIVER FUNCTIONALITY DENIED | Informational | - |
// Informational
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/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:
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
);
}
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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);
}
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 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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed