Platform: Code4rena
Start Date: 27/04/2022
Pot Size: $50,000 MIM
Total HM: 6
Participants: 59
Period: 5 days
Judge: 0xean
Id: 113
League: ETH
Rank: 1/59
Findings: 6
Award: $8,053.09
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: BowTiedWardens
In NFTPairWithOracle._lend
, params.oracle
is not checked. This allow a borrower to watch the mempool and front-run the lender's call and change oracle
to avoid liquidation.
function _lend( address lender, uint256 tokenId, TokenLoanParams memory accepted, bool skim ) internal { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); TokenLoanParams memory params = tokenLoanParams[tokenId]; // Valuation has to be an exact match, everything else must be at least // as good for the lender as `accepted`. require( params.valuation == accepted.valuation && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && params.ltvBPS >= accepted.ltvBPS, "NFTPair: bad params" );
require( params.valuation == accepted.valuation && params.oracle == accepted.oracle && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && params.ltvBPS <= accepted.ltvBPS, "NFTPair: bad params" );
#0 - cryptolyndon
2022-05-05T23:56:09Z
Confirmed. Duplicate of #136
🌟 Selected for report: 0x1337
Also found by: BowTiedWardens, GimelSec, catchup, cccz, gzeon, horsefacts, hyh, kenzo
In NFTPairWithOracle.updateLoanParams
, a lender is allowed change the oracle
. If the lender set it some oracle that return invalid price, he can call removeCollateral
immediately to liquidate the borrower.
function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { // The lender can change terms so long as the changes are strictly // the same or better for the borrower: require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS <= cur.ltvBPS, "NFTPair: worse params" );
require(rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued");
require( params.duration >= cur.duration && params.oracle == cur.oracle && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS >= cur.ltvBPS, "NFTPair: worse params" );
#0 - cryptolyndon
2022-05-05T23:54:47Z
Confirmed. Duplicate of #37.
In NFTPairWithOracle.updateLoanParams
, a lender is allowed to decrease ltvBPS
. If the lender set it to 0, he can call removeCollateral
immediately to liquidate the borrower.
function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { // The lender can change terms so long as the changes are strictly // the same or better for the borrower: require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS <= cur.ltvBPS, "NFTPair: worse params" );
require(rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued");
require( params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS >= cur.ltvBPS, "NFTPair: worse params" );
#0 - cryptolyndon
2022-05-05T23:54:12Z
Confirmed. Duplicate of #51.
In NFTPairWithOracle._lend
, the loan ltvBPS can be higher than the lender's accepted ltvBPS
. This allow a borrower to watch the mempool and front-run the lender's call and change ltvBPS
to some very large value using updateLoanParams
to avoid liquidation.
function _lend( address lender, uint256 tokenId, TokenLoanParams memory accepted, bool skim ) internal { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); TokenLoanParams memory params = tokenLoanParams[tokenId]; // Valuation has to be an exact match, everything else must be at least // as good for the lender as `accepted`. require( params.valuation == accepted.valuation && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && params.ltvBPS >= accepted.ltvBPS, "NFTPair: bad params" );
require( params.valuation == accepted.valuation && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && params.ltvBPS <= accepted.ltvBPS, "NFTPair: bad params" );
#0 - cryptolyndon
2022-05-05T23:55:39Z
Confirmed. Duplicate of #55.
🌟 Selected for report: IllIllI
Also found by: 0x1337, 0x1f8b, 0xDjango, 0xf15ers, AuditsAreUS, BowTiedWardens, CertoraInc, Funen, GimelSec, MaratCerby, Ruhum, WatchPug, antonttc, berndartmueller, bobi, bobirichman, broccolirob, catchup, cccz, defsec, delfin454000, gs8nrv, gzeon, horsefacts, hubble, hyh, ilan, jah, joestakey, kebabsec, kenta, kenzo, m9800, mics, oyc_109, pauliax, reassor, robee, samruna, sikorico, simon135, throttle, unforgiven, z3s
72.9199 MIM - $72.92
init
of the master contract can be called by anyoneWhile there seems to be no exploit, we can set collateral to a non-zero value in constructor to reduce risk. https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPair.sol#L175
asset
function init(bytes calldata data) public payable override { require(address(collateral) == address(0), "NFTPair: already initialized"); (collateral, asset) = abi.decode(data, (IERC721, IERC20)); require(address(collateral) != address(0), "NFTPair: bad pair"); }
collateral.transferFrom(address(this), to, tokenId);
Consider to pin Solidity version to latest 0.8.12
#0 - cryptolyndon
2022-05-13T05:08:03Z
Checking asset
is not necessary; zero is no worse than any other value. The collateral
field happens to be used to ensure the method can only be called once. For that reason, setting it to something other than zero in the constructor would break things.
Suppose I can't argue with the empty revert()
..
🌟 Selected for report: BowTiedWardens
Also found by: 0x1f8b, 0xNazgul, 0xf15ers, 0xkatana, CertoraInc, Funen, GimelSec, Hawkeye, IllIllI, Kulk0, NoamYakov, Tadashi, Tomio, TrungOre, antonttc, catchup, defsec, delfin454000, fatherOfBlocks, gzeon, horsefacts, joestakey, kenta, oyc_109, pauliax, reassor, robee, samruna, simon135, slywaters, sorrynotsorry, z3s
45.4061 MIM - $45.41
Use ++i instead of i++ can save gas
contracts/NFTPairWithOracle.sol:527: for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) { contracts/NFTPairWithOracle.sol:674: for (uint256 i = 0; i < actions.length; i++) { contracts/NFTPair.sol:494: for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) { contracts/NFTPair.sol:641: for (uint256 i = 0; i < actions.length; i++) {
For interest rate < 20%, N = 2 is enough https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPair.sol#L494-L495
for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {
#0 - cryptolyndon
2022-05-14T01:45:27Z
Seen, thanks
We'd have to put tests somewhere, especially if using more than two buckets, and arithmetic is pretty cheap. But the only one to mention this.