Platform: Code4rena
Start Date: 25/10/2022
Pot Size: $50,000 USDC
Total HM: 18
Participants: 127
Period: 5 days
Judge: 0xean
Total Solo HM: 9
Id: 175
League: ETH
Rank: 16/127
Findings: 3
Award: $537.86
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: 0x1f8b, 0xNazgul, 0xc0ffEE, 8olidity, Aymen0909, Chom, Franfran, Jeiwan, Jujic, Lambda, M4TZ1P, Olivierdem, Rolezn, Ruhum, TomJ, Wawrdog, __141345__, bin2chen, c7e7eff, carlitox477, catchup, cccz, codexploder, cuteboiz, d3e4, dipp, djxploit, eierina, elprofesor, hansfriese, horsefacts, idkwhatimdoing, imare, immeas, joestakey, ladboy233, leosathya, martin, minhtrng, pashov, peanuts, pedroais, rokinot, rvierdiiev, saneryee, sorrynotsorry, tonisives
0.385 USDC - $0.38
https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L82 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L116
According to Chainlink's documentation, the latestAnswer
function is deprecated.
This function does not error if no answer has been reached but returns 0. Besides, the latestAnswer
is reported with 18 decimals for crypto quotes but 8 decimals for FX quotes (See Chainlink FAQ for more details). And also, is not possible to check if the price is fresh.
Affected source code:
latestRoundData
function to get the price instead. Add checks on the return data with proper revert messages if the price is stale or the round is uncomplete, for example:(uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound) = feeds[token].feed.latestRoundData(); require(answeredInRound >= roundID, "..."); require(timeStamp != 0, "...");
#0 - neumoxx
2022-10-31T08:47:53Z
Duplicate of #601
#1 - c4-judge
2022-11-05T17:55:21Z
0xean marked the issue as duplicate
#2 - Simon-Busch
2022-12-05T15:23:52Z
Issue marked as satisfactory as requested by 0xean
#3 - c4-judge
2022-12-07T08:14:13Z
Simon-Busch marked the issue as duplicate of #584
🌟 Selected for report: 0x1f8b
Also found by: 0xNazgul, 0xSmartContract, Aymen0909, B2, Bnke0x0, Deivitto, Diana, Dinesh11G, ElKu, JC, Josiah, Rahoz, RaymondFam, ReyAdmirado, Rolezn, Waze, __141345__, adriro, aphak5010, brgltd, c3phas, c7e7eff, carlitox477, cducrest, ch0bu, chrisdior4, cryptonue, cryptostellar5, cylzxje, d3e4, delfin454000, enckrish, evmwanderer, fatherOfBlocks, gogo, hansfriese, horsefacts, immeas, leosathya, lukris02, neumo, oyc_109, pedr02b2, rbserver, robee, rotcivegaf, rvierdiiev, sakshamguruji, shark, simon135, tnevler, trustindistrust, wagmi
518.4669 USDC - $518.47
SECP256K1
signaturesHere, the ecrecover()
method doesn't check the s
range.
Homestead (EIP-2) added this limitation, however the precompile remained unaltered. The majority of libraries, including OpenZeppelin, do this check.
Since an order can only be confirmed once and its hash is saved, there doesn't seem to be a serious danger in existing use cases.
Reference:
Affected source code:
address(0)
The following methods have a lack of checks if the received argument is an address, it's good practice in order to reduce human error to check that the address specified in the constructor or initialize is different than address(0)
.
Affected source code:
tx.origin
tx.origin
is a global variable in Solidity that returns the address of the account that sent the transaction.
Using the variable could make a contract vulnerable if an authorized account calls a malicious contract. You can impersonate a user using a third party contract.
This can make it easier to create a vault on behalf of another user with an external administrator (by receiving it as an argument).
Affected source code:
The pragma version used are:
pragma solidity ^0.8.13;
Note that mixing pragma is not recommended. Because different compiler versions have different meanings and behaviors, it also significantly raises maintenance costs. As a result, depending on the compiler version selected for any given file, deployed contracts may have security issues.
The minimum required version must be 0.8.17; otherwise, contracts will be affected by the following important bug fixes:
calldatasize()
in all cases.bytes
arrays.Apart from these, there are several minor bug fixes and improvements.
It's possible to lose the ownership under specific circumstances.
Because of human error it's possible to set a new invalid owner. When you want to change the owner's address it's better to propose a new owner, and then accept this ownership with the new wallet.
Affected source code:
contraction
In the Fed
contract, during the expansion
method is checked that the market is not paused, this requirement is not done during the contraction
.
function contraction(IMarket market, uint amount) public { require(msg.sender == chair, "ONLY CHAIR"); require(dbr.markets(address(market)), "UNSUPPORTED MARKET"); + require(!market.borrowPaused(), "CANNOT EXPAND PAUSED MARKETS"); uint supply = supplies[market]; require(amount <= supply, "AMOUNT TOO BIG"); // can't burn profits market.recall(amount); dola.burn(amount); supplies[market] -= amount; globalSupply -= amount; emit Contraction(market, amount); }
Affected source code:
The Market.getEscrow
, Fed.expansion
and Fed.contraction
methods do not have the noReentrant
modifier and make calls to an external contract that can take advantage of and call these methods again, but it seems to fail due to the lack of tokens.
However, if any of the other addresses used their receive event to provide liquidity to the contract, the attacking account could benefit from it.
- function expansion(IMarket market, uint amount) public { + function expansion(IMarket market, uint amount) public noReentrant { ... } - function contraction(IMarket market, uint amount) public { + function contraction(IMarket market, uint amount) public noReentrant { ... }
For example, in getEscrow
if the escrow
allows a callback, it could create two scrows, loosing funds if in this callback it will call again getEscrow
, using for example deposit
function getEscrow(address user) internal returns (IEscrow) { if(escrows[user] != IEscrow(address(0))) return escrows[user]; IEscrow escrow = createEscrow(user); escrow.initialize(collateral, user); escrows[user] = escrow; return escrow; }
deposit
.escrow
initialization it happend a reentrancy and call again deposit
.Please note that current escrows do not allow re-entry, so I decided to use Low. It's always good to change the storage flags before the externals calls.
Affected source code:
The following methods lack checks on the following integer arguments, you can see the recommendations above.
Affected source code:
_replenishmentPriceBps
is not checked to be != 0 during the constructor
, nevertheless it's checked in setReplenishmentPriceBps
replenishmentIncentiveBps
is not checked to be > 0 during the constructor
, nevertheless it's checked in setReplenismentIncentiveBps
supportsInterface
The EIP-165
standard helps detect that a smart contract implements the expected logic, prevents human error when configuring smart contract bindings, so it is recommended to check that the received argument is a contract and supports the expected interface.
Reference:
Affected source code:
The Market.pauseBorrows
, Market.setLiquidationFeeBps
, Market.setLiquidationIncentiveBps
, Market.setReplenismentIncentiveBps
, Market.setLiquidationFactorBps
, Market.setCollateralFactorBps
, Market.setBorrowController
, Market.setOracle
methods do not emit an event when the state changes, something that it's very important for dApps and users.
Affected source code:
Keep in mind that the version of solidity used, despite being greater than 0.8
, does not prevent integer overflows during casting, it only does so in mathematical operations.
In the case that feed.decimals()
returns 18, and the token is more than 18 decimals, the following subtraction will cause an underflow, denying the oracle service.
uint8 feedDecimals = feeds[token].feed.decimals(); // 18 => [ETH/DAI] https://rinkeby.etherscan.io/address/0x74825dbc8bf76cc4e9494d0ecb210f676efa001d#readContract uint8 tokenDecimals = feeds[token].tokenDecimals; // > 18 uint8 decimals = 36 - feedDecimals - tokenDecimals; // overflow
All pairs have 8 decimals except the ETH pairs, so a token with 19 decimals in ETH, will fault.
Affected source code:
The method accrueDueTokens
doesn't check that the call is made by a market, and it's public, it should be changed to internal or private to be more resilient.
require(markets[msg.sender], "Only markets can call onBorrow");
Affected source code:
The interface IERC20
contains two methdos that are not pressent in the official ERC20, delegate
and delegates
, it's recommended to change the name of the contract because not any ERC20 it's valid.
Affected source code:
The code that contains "open todos" reflects that the development is not finished and that the code can change a posteriori, prior release, with or without audit.
Affected source code:
// TODO: Test whether an immutable variable will persist across proxies
The viewPrice
and getPrice
methods of the Oracle
contract are very similar, the only difference being the following peace of code:
if(todaysLow == 0 || normalizedPrice < todaysLow) { dailyLows[token][day] = normalizedPrice; todaysLow = normalizedPrice; emit RecordDailyLow(token, normalizedPrice); }
It's recommended to reuse the code in order to be more readable and light.
Affected source code:
It is not good practice to hardcode values, but if you are dealing with addresses much less, these can change between implementations, networks or projects, so it is convenient to remove these values from the source code.
Affected source code:
It's recommended to create a factor variable for 10000
:
#0 - c4-judge
2022-11-08T00:57:31Z
0xean marked the issue as grade-a
#1 - c4-judge
2022-11-08T01:10:31Z
0xean marked the issue as selected for report
🌟 Selected for report: pfapostol
Also found by: 0x1f8b, 0xRoxas, 0xSmartContract, Amithuddar, Aymen0909, B2, Bnke0x0, Chandr, CloudX, Deivitto, Diana, Dinesh11G, ElKu, HardlyCodeMan, JC, JrNet, KoKo, Mathieu, Ozy42, Rahoz, RaymondFam, ReyAdmirado, Rolezn, Shinchan, __141345__, adriro, ajtra, aphak5010, ballx, c3phas, carlitox477, ch0bu, chaduke, cryptostellar5, djxploit, durianSausage, enckrish, exolorkistis, fatherOfBlocks, gogo, horsefacts, kaden, karanctf, leosathya, martin, mcwildy, oyc_109, ret2basic, robee, sakman, sakshamguruji, shark, skyle, tnevler
19.0072 USDC - $19.01
Compare a boolean value using == true
or == false
instead of using the boolean value is more expensive.
NOT
opcode it's cheaper than using EQUAL
or NOTEQUAL
when the value it's false, or just the value without == true
when it's true, because it will use less opcodes inside the VM.
Proof of concept (without optimizations):
pragma solidity 0.8.16; contract TesterA { function testEqual(bool a) public view returns (bool) { return a == true; } } contract TesterB { function testNot(bool a) public view returns (bool) { return a; } }
Gas saving executing: 18 per entry for == true
TesterA.testEqual: 21814 TesterB.testNot: 21796
pragma solidity 0.8.16; contract TesterA { function testEqual(bool a) public view returns (bool) { return a == false; } } contract TesterB { function testNot(bool a) public view returns (bool) { return !a; } }
Gas saving executing: 15 per entry for == false
TesterA.testEqual: 21814 TesterB.testNot: 21799
Affected source code:
Use the value instead of != true
:
Using compound assignment operators for state variables (like State += X
or State -= X
...) it's more expensive than using operator assignment (like State = State + X
or State = State - X
...).
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { uint private _a; function testShort() public { _a += 1; } } contract TesterB { uint private _a; function testLong() public { _a = _a + 1; } }
Gas saving executing: 13 per entry
TesterA.testShort: 43507 TesterB.testLong: 43494
Affected source code:
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { function testShortRevert(bool path) public view { require(path, "test error"); } } contract TesterB { function testLongRevert(bool path) public view { require(path, "test big error message, more than 32 bytes"); } }
Gas saving executing: 18 per entry
TesterA.testShortRevert: 21886 TesterB.testLongRevert: 21904
Affected source code:
Having a method that always returns the same value is not correct in terms of consumption, if you want to modify a value, and the method will perform a revert
in case of failure, it is not necessary to return a true
, since it will never be false
. It is less expensive not to return anything, and it also eliminates the need to double-check the returned value by the caller.
Affected source code:
delete
optimizationUse delete
instead of set to default value (false
or 0
).
5 gas could be saved per entry in the following affected lines:
Affected source code:
unchecked
keywordWhen an underflow or overflow cannot occur, one might conserve gas by using the unchecked
keyword to prevent unnecessary arithmetic underflow/overflow tests.
function contraction(IMarket market, uint amount) public { require(msg.sender == chair, "ONLY CHAIR"); require(dbr.markets(address(market)), "UNSUPPORTED MARKET"); uint supply = supplies[market]; require(amount <= supply, "AMOUNT TOO BIG"); // can't burn profits market.recall(amount); dola.burn(amount); + unckeched { supplies[market] -= amount; globalSupply -= amount; + } emit Contraction(market, amount); }
function getProfit(IMarket market) public view returns (uint) { uint marketValue = dola.balanceOf(address(market)) + market.totalDebt(); uint supply = supplies[market]; if(supply >= marketValue) return 0; + unckeched { return marketValue - supply; + } }
globalSupply
don't overflow, supplies
also won't overflow.function expansion(IMarket market, uint amount) public { require(msg.sender == chair, "ONLY CHAIR"); require(dbr.markets(address(market)), "UNSUPPORTED MARKET"); require(market.borrowPaused() != true, "CANNOT EXPAND PAUSED MARKETS"); dola.mint(address(market), amount); + globalSupply += amount; + unckeched { supplies[market] += amount; + } - globalSupply += amount; require(globalSupply <= supplyCeiling); emit Expansion(market, amount); }
function totalSupply() public view returns (uint) { - if(totalDueTokensAccrued > _totalSupply) return 0; + if(totalDueTokensAccrued >= _totalSupply) return 0; + unckeched { return _totalSupply - totalDueTokensAccrued; + } }
function balanceOf(address user) public view returns (uint) { uint debt = debts[user]; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; - if(dueTokensAccrued[user] + accrued > balances[user]) return 0; - return balances[user] - dueTokensAccrued[user] - accrued; + accrued += dueTokensAccrued[user]; + if(accrued >= balances[user]) return 0; + unckeched { + return balances[user] - accrued; + } }
function deficitOf(address user) public view returns (uint) { uint debt = debts[user]; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; - if(dueTokensAccrued[user] + accrued < balances[user]) return 0; - return dueTokensAccrued[user] + accrued - balances[user]; + accrued += dueTokensAccrued[user]; + if(accrued <= balances[user]) return 0; + unckeched { + return accrued - balances[user]; + } }
totalDueTokensAccrued
don't overflow, dueTokensAccrued
also won't overflow.function accrueDueTokens(address user) public { uint debt = debts[user]; if(lastUpdated[user] == block.timestamp) return; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; + totalDueTokensAccrued += accrued; + unckeched { dueTokensAccrued[user] += accrued; + } - totalDueTokensAccrued += accrued; lastUpdated[user] = block.timestamp; emit Transfer(user, address(0), accrued); }
function getWithdrawalLimitInternal(address user) internal returns (uint) { IEscrow escrow = predictEscrow(user); uint collateralBalance = escrow.balance(); if(collateralBalance == 0) return 0; uint debt = debts[user]; if(debt == 0) return collateralBalance; if(collateralFactorBps == 0) return 0; uint minimumCollateral = debt * 1 ether / oracle.getPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; if(collateralBalance <= minimumCollateral) return 0; + unckeched { return collateralBalance - minimumCollateral; + } }
function getWithdrawalLimit(address user) public view returns (uint) { IEscrow escrow = predictEscrow(user); uint collateralBalance = escrow.balance(); if(collateralBalance == 0) return 0; uint debt = debts[user]; if(debt == 0) return collateralBalance; if(collateralFactorBps == 0) return 0; uint minimumCollateral = debt * 1 ether / oracle.viewPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; if(collateralBalance <= minimumCollateral) return 0; + unckeched { return collateralBalance - minimumCollateral; + } }
debts[borrower]
don't overflow, totalDebt
also won't overflow.function borrowInternal(address borrower, address to, uint amount) internal { require(!borrowPaused, "Borrowing is paused"); if(borrowController != IBorrowController(address(0))) { require(borrowController.borrowAllowed(msg.sender, borrower, amount), "Denied by borrow controller"); } uint credit = getCreditLimitInternal(borrower); debts[borrower] += amount; require(credit >= debts[borrower], "Exceeded credit limit"); + unckeched { totalDebt += amount; + } dbr.onBorrow(borrower, amount); dola.transfer(to, amount); emit Borrow(borrower, amount); }
function repay(address user, uint amount) public { - uint debt = debts[user]; - require(debt >= amount, "Insufficient debt"); + require(debts[user] >= amount, "Insufficient debt"); + unckeched { debts[user] -= amount; totalDebt -= amount; + } dbr.onRepay(user, amount); dola.transferFrom(msg.sender, address(this), amount); emit Repay(user, msg.sender, amount); }
function liquidate(address user, uint repaidDebt) public { require(repaidDebt > 0, "Must repay positive debt"); uint debt = debts[user]; require(getCreditLimitInternal(user) < debt, "User debt is healthy"); require(repaidDebt <= debt * liquidationFactorBps / 10000, "Exceeded liquidation factor"); uint price = oracle.getPrice(address(collateral), collateralFactorBps); uint liquidatorReward = repaidDebt * 1 ether / price; liquidatorReward += liquidatorReward * liquidationIncentiveBps / 10000; + unckeched { debts[user] -= repaidDebt; totalDebt -= repaidDebt; + } dbr.onRepay(user, repaidDebt); dola.transferFrom(msg.sender, address(this), repaidDebt); IEscrow escrow = predictEscrow(user); escrow.pay(msg.sender, liquidatorReward); if(liquidationFeeBps > 0) { uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000; if(escrow.balance() >= liquidationFee) { escrow.pay(gov, liquidationFee); } } emit Liquidate(user, msg.sender, repaidDebt, liquidatorReward); }
Affected source code:
The following state variables can be removed without affecting the logic of the contract since they are not used and/or are redundant because they could be used inline.
function contraction(IMarket market, uint amount) public { require(msg.sender == chair, "ONLY CHAIR"); require(dbr.markets(address(market)), "UNSUPPORTED MARKET"); - uint supply = supplies[market]; - require(amount <= supply, "AMOUNT TOO BIG"); // can't burn profits + require(amount <= supplies[market], "AMOUNT TOO BIG"); // can't burn profits market.recall(amount); dola.burn(amount); supplies[market] -= amount; globalSupply -= amount; emit Contraction(market, amount); }
Affected source code:
The viewPrice
and getPrice
methods of the Oracle
contract are very similar, the only difference being the following peace of code:
if(todaysLow == 0 || normalizedPrice < todaysLow) { dailyLows[token][day] = normalizedPrice; todaysLow = normalizedPrice; emit RecordDailyLow(token, normalizedPrice); }
It's recommended to reuse the code in order to be more readable and smaller for deployment.
Affected source code:
function getEscrow(address user) internal returns (IEscrow) { + IEscrow escrow = escrows[user]; + if(escrow != IEscrow(address(0))) return escrow; - if(escrows[user] != IEscrow(address(0))) return escrows[user]; - IEscrow escrow = createEscrow(user); + escrow = createEscrow(user); + escrows[user] = escrow; escrow.initialize(collateral, user); - escrows[user] = escrow; return escrow; }
Affected source code:
Market.predictEscrow
The call to predictEscrow
supposes an unnecessary expense in the following methods, this is because in the case of not existing in the mapping escrows[user]
will not have been created yet, and the balance
or pay
methods cannot be called if was not created.
It's recommended to use the stored value escrows[user]
.
Affected source code:
DBR.accrueDueTokens
The method DBR.accrueDueTokens
doesn't check that the call is made by a market, and it's public, it should be changed to internal or private to save gas, and move the debt
variable to avoid waste gas when it's fresh.
function accrueDueTokens(address user) internal { - uint debt = debts[user]; if(lastUpdated[user] == block.timestamp) return; + uint debt = debts[user]; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; dueTokensAccrued[user] += accrued; totalDueTokensAccrued += accrued; lastUpdated[user] = block.timestamp; emit Transfer(user, address(0), accrued); }
Affected source code:
#0 - c4-judge
2022-11-05T23:55:48Z
0xean marked the issue as grade-b