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: 13/127
Findings: 4
Award: $712.25
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: RaoulSchaffranek
Also found by: carlitox477
294.0444 USDC - $294.04
https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/DBR.sol#L284-L292 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/DBR.sol#L122 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/DBR.sol#L135 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/DBR.sol#L148
Allows updating lastUpdated[user]
, what would affect next accrueDueTokens calculations, affecting dueTokensAccrued[user]
, and functions balanceOf, deficitOf and signedBalanceOf if they are called in next blocks
accrueDueTokens
function won't update his dueTokensAccrued balance, so he program a bot which allows him to extend his debt by calling accrueDueTokens
function considering (block.timestamp - lastUpdated[user]) * 365 * 60 < 365 * 24 * 60 * 60
then (block.timestamp - lastUpdated[user]) < 24 * 60
. This mean that if a bot do a call in a block which is emited as long as 24 minutes after last call to accrueDueTokens
, Bob will extend his borrow time.If (block.timestamp - lastUpdated[user]) * debt / 365 days = 0
skip next lines in function accrueDueTokens
. This means:
function accrueDueTokens(address user) public { uint debt = debts[user]; if(lastUpdated[user] == block.timestamp) return; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; if(accrued != 0){ dueTokensAccrued[user] += accrued; totalDueTokensAccrued += accrued; lastUpdated[user] = block.timestamp; emit Transfer(user, address(0), accrued); } }
#0 - 0xean
2022-11-05T20:52:24Z
this amounts to a rounding error / dust.
#1 - c4-judge
2022-11-05T20:52:44Z
#2 - c4-judge
2022-11-05T20:52:45Z
0xean marked the issue as change severity
#3 - c4-judge
2022-12-06T18:54:21Z
0xean marked the issue as duplicate of #83
#4 - c4-judge
2022-12-06T18:54:28Z
0xean marked the issue as partial-25
🌟 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#L116 https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L82
In the current implementation of Oracle.sol#getPrice(), there is no freshness check. This could lead to stale prices being used.
https://github.com/sherlock-audit/2022-08-sentiment-judging/blob/main/002-M/1-report.md https://github.com/code-423n4/2022-04-backd-findings/issues/17
Consider adding the missing freshness check for stale price by adding a validPeriod for each token by adding state variable mapping (address => FeedData) public tokensValidPeriods;
, then:
function setValidPeriod(address token, uint256 validPeriod) public onlyOperator { if(feeds[token] == address(0)) revert(ERROR_NO_DATA_FEED_SET(token)); tokensValidPeriods[token] = validPeriod; } function getPrice(address token, uint collateralFactorBps) external returns (uint) { if(fixedPrices[token] > 0) return fixedPrices[token]; if(feeds[token].feed != IChainlinkFeed(address(0))) { // get price from feed // @audit Modifation (uint80 roundID, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feeds[token].feed.latestRoundData(); require(price > 0, "Invalid feed price"); require(answeredInRound >= roundID, "Stale price"); require(block.timestamp <= updatedAt + tokensValidPeriods[token], "Stale price"); // normalize price uint8 feedDecimals = feeds[token].feed.decimals(); uint8 tokenDecimals = feeds[token].tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals); // potentially store price as today's low uint day = block.timestamp / 1 days; uint todaysLow = dailyLows[token][day]; if(todaysLow == 0 || normalizedPrice < todaysLow) { dailyLows[token][day] = normalizedPrice; todaysLow = normalizedPrice; emit RecordDailyLow(token, normalizedPrice); } // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor uint newBorrowingPower = normalizedPrice * collateralFactorBps / 10000; uint twoDayLow = todaysLow > yesterdaysLow && yesterdaysLow > 0 ? yesterdaysLow : todaysLow; if(twoDayLow > 0 && newBorrowingPower > twoDayLow) { uint dampenedPrice = twoDayLow * 10000 / collateralFactorBps; return dampenedPrice < normalizedPrice ? dampenedPrice: normalizedPrice; } return normalizedPrice; } revert("Price not found"); } function viewPrice(address token, uint collateralFactorBps) external view returns (uint) { if(fixedPrices[token] > 0) return fixedPrices[token]; if(feeds[token].feed != IChainlinkFeed(address(0))) { // get price from feed // @audit Modifation (uint80 roundID, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feeds[token].feed.latestRoundData(); require(price > 0, "Invalid feed price"); require(answeredInRound >= roundID, "Stale price"); require(block.timestamp <= updatedAt + tokensValidPeriods[token], "Stale price"); // normalize price uint8 feedDecimals = feeds[token].feed.decimals(); uint8 tokenDecimals = feeds[token].tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals); uint day = block.timestamp / 1 days; // get today's low uint todaysLow = dailyLows[token][day]; // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor uint newBorrowingPower = normalizedPrice * collateralFactorBps / 10000; uint twoDayLow = todaysLow > yesterdaysLow && yesterdaysLow > 0 ? yesterdaysLow : todaysLow; if(twoDayLow > 0 && newBorrowingPower > twoDayLow) { uint dampenedPrice = twoDayLow * 10000 / collateralFactorBps; return dampenedPrice < normalizedPrice ? dampenedPrice: normalizedPrice; } return normalizedPrice; } revert("Price not found"); }
#0 - c4-judge
2022-11-05T17:50:39Z
0xean marked the issue as duplicate
#1 - Simon-Busch
2022-12-05T15:26:08Z
Issue marked as satisfactory as requested by 0xean
#2 - 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
398.8207 USDC - $398.82
This would let unusable next functions:
This can be avoided by a check
Setting the pending operator to current operator would allow to emit a nonsense event ChangeOperator
when claimOperator
is called
Good practise, avoid setting it to zero address
Giving the lack of zero address check, beneficiary
can be set to address zero with no way of change it value, leaving delegate function useless.
Giving they are not used by any other function in the contract the visibility of next function should be external:
Giving the lack of zero address check, _xINV
can be set to address zero with no way of change it value, leaving initialize; pay; balance; onDeposit and delegate functions useless, leading to contract redeployment.
Add next line to the constructor:
if(_INVX == _INVX(address(0))) revert ERROR_ADDRESS_ZERO_SETTING()
Giving they are not used by any other function in the contract the visibility of next function should be external:
Giving the lack of zero address check, beneficiary
can be set to address zero with no way of change it value, leaving delegate function useless.
Giving the lack of zero address check, _token
can be set to address zero with no way of change it value, leaving pay and balance functions useless, leading to redeployment.
Contract creator can create a contract with operator set to zero address.
If address zero is set, next functions would be affected:
Recommended code:
constructor(address _operator) { if(_operator == address(0)) revert ERROR_INVALID_OPERATOR(); operator = _operator; }
Contract operator can set new operator to to zero address.
If address zero is set, next functions would be affected:
Recommended code:
function setOperator(address _operator) public onlyOperator { if(_operator == address(0)) revert ERROR_INVALID_OPERATOR(); operator = _operator; }
Functions affected:
Is a good practise to set important roles values in two steps. So a state variable pendingOperator
should be added and this function should be replace for:
modifier onlyPendingOperator { if(msg.sender != pendingOperator) revert ERROR_NOT_PENDING_OPERATOR() _; } function setPendingOperator(address _pendingOperator) external onlyOperator { pendingOperator = _pendingOperator; } function claimOperator(address _pendingOperator) external onlyPendingOperator { pendingOperator = address(0); operator = msg.sender; }
Giving they are not used by any other function in the contract the visibility of next function should be external:
This would leave next function useless:
As a consequence, the contract will need to be redeployed.
Mitigation steps: add next line to the constructor
if (_operator == address(0)) revert ERROR_ADDRESS_ZERO_ASIGNEMENT();
This functions are:
pendingOperator == operator
. This condition should be checked to avoid a meaningless ChangeOperator
event emission minters[minter_] == true
. This condition should be checked to avoid a meaningless AddMinter
event emission minters[minter_] == false
. This condition should be checked to avoid a meaningless RemoveMinter
event emissionmarkets[market_] == true
. This condition should be checked to avoid a meaningless AddMarket
event emissionreplenishmentPriceBps
seems to be used just to calculate a porcentage for amount
in function onForceReplenish
:
uint replenishmentCost = amount * replenishmentPriceBps / 10000;
However if replenishmentPriceBps > 10000
the replenishmentCost will be a multiple of amount.
For this reason, setReplenishmentPriceBps
should check that newReplenishmentPriceBps_
is lower/equal than 10.000
Oherwise next function may become useless
This in order to follow best practises and avoid setting gov
to zero address. So, replace changeGov
for:
address pendingGov; function setPendingGov(address _pendigGov) public { require(msg.sender == gov, "ONLY GOV"); pendingGov = _pendigGov; } function claimGov() public { require(msg.sender == pendingGov, "ONLY GOV"); gov = msg.sender; pendingGov = addres(0); }
Otherwise every function with onlyGov
modifer will become inaccesible.
This in order to follow best practises and avoid setting gov
to zero address. So, replace setGov
for:
address pendingGov; function setPendingGov(address _pendigGov) public { require(msg.sender == gov, "ONLY GOV"); pendingGov = _pendigGov; } function claimGov() public { require(msg.sender == pendingGov, "ONLY GOV"); gov = msg.sender; pendingGov = addres(0); }
#0 - c4-judge
2022-11-08T00:57:01Z
0xean marked the issue as grade-a
🌟 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
Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table.
Change this line for:
IXINV private immutable xINV;
This will save gas, replace this function for:
function pay(address recipient, uint amount) public { IERC20 _token = token; require(msg.sender == market, "ONLY MARKET"); uint invBalance = _token.balanceOf(address(this)); if(invBalance < amount) xINV.redeemUnderlying(amount - invBalance); // we do not check return value because next call will fail if this fails anyway _token.transfer(recipient, amount); }
The second parameter is never used, so it can be deleted in order to save gas. Change this line for:
function initialize(IERC20 _token) external {
The second parameter is never used, so it can be deleted in order to save gas. Change this line for:
function borrowAllowed(address msgSender) external view returns (bool) {
This will save gas during contract creation.
This will save gas in function balanceOf.
function balanceOf(address user) public view returns (uint) { uint debt = debts[user]; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; // accrued debt? uint256 _dueTokensAccrued = dueTokensAccrued[user]; uint256 _balance = balances[user]; if(_dueTokensAccrued+ accrued > _balance) return 0; return _balance - _dueTokensAccrued - accrued; }
To avoid 3 state variable access to globalSupply replace this code for:
_globalSupply = globalSupply + amount; require(_globalSupply <= supplyCeiling); globalSupply = _globalSupply;
Replace this line of code for supplies[market] = supply - amount;
Change claimOperator to
function claimOperator() public { require(msg.sender == pendingOperator, "ONLY PENDING OPERATOR"); operator = msg.sender; pendingOperator = address(0); emit ChangeOperator(msg.sender); }
Change viewPrice to
function viewPrice(address token, uint collateralFactorBps) external view returns (uint) { uint256 _fixedPrice= fixedPrices[token] if(_fixedPrice > 0) return _fixedPrice; IChainlinkFeed _feed = feeds[token].feed if(_feed.feed != IChainlinkFeed(address(0))) { // get price from feed uint price = _feed.feed.latestAnswer(); require(price > 0, "Invalid feed price"); // normalize price uint8 feedDecimals = _feed.feed.decimals(); uint8 tokenDecimals = _feed.tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals); uint day = block.timestamp / 1 days; // get today's low uint todaysLow = dailyLows[token][day]; // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor uint newBorrowingPower = normalizedPrice * collateralFactorBps / 10000; uint twoDayLow = todaysLow > yesterdaysLow && yesterdaysLow > 0 ? yesterdaysLow : todaysLow; if(twoDayLow > 0 && newBorrowingPower > twoDayLow) { uint dampenedPrice = twoDayLow * 10000 / collateralFactorBps; return dampenedPrice < normalizedPrice ? dampenedPrice: normalizedPrice; } return normalizedPrice; } revert("Price not found"); }
Change getPrice to
function getPrice(address token, uint collateralFactorBps) external returns (uint) { uint256 _fixedPrice= fixedPrices[token] if(_fixedPrice > 0) return _fixedPrice; IChainlinkFeed _feed = feeds[token] if(_feed.feed != IChainlinkFeed(address(0))) { // get price from feed uint price = _feed.feed.latestAnswer(); require(price > 0, "Invalid feed price"); // normalize price uint8 feedDecimals = _feed.feed.decimals(); uint8 tokenDecimals = _feed.tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals); // potentially store price as today's low uint day = block.timestamp / 1 days; uint todaysLow = dailyLows[token][day]; if(todaysLow == 0 || normalizedPrice < todaysLow) { dailyLows[token][day] = normalizedPrice; todaysLow = normalizedPrice; emit RecordDailyLow(token, normalizedPrice); } // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor uint newBorrowingPower = normalizedPrice * collateralFactorBps / 10000; uint twoDayLow = todaysLow > yesterdaysLow && yesterdaysLow > 0 ? yesterdaysLow : todaysLow; if(twoDayLow > 0 && newBorrowingPower > twoDayLow) { uint dampenedPrice = twoDayLow * 10000 / collateralFactorBps; return dampenedPrice < normalizedPrice ? dampenedPrice: normalizedPrice; } return normalizedPrice; } revert("Price not found"); }
#0 - c4-judge
2022-11-05T23:43:03Z
0xean marked the issue as grade-b