Platform: Code4rena
Start Date: 03/11/2022
Pot Size: $115,500 USDC
Total HM: 17
Participants: 120
Period: 7 days
Judge: LSDan
Total Solo HM: 1
Id: 174
League: ETH
Rank: 51/120
Findings: 2
Award: $110.58
π Selected for report: 0
π Solo Findings: 0
π Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xRoxas, 0xSmartContract, Awesome, Aymen0909, B2, BClabs, Bnke0x0, Deekshith99, Deivitto, Diana, Dinesh11G, Funen, HE1M, HardlyCodeMan, Josiah, Nyx, Rahoz, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Saintcode_, TomJ, Trust, __141345__, a12jmx, adriro, ajtra, aphak5010, apostle0x01, brgltd, btk, bulej93, c3phas, carlitox477, catwhiskeys, ch0bu, chaduke, chrisdior4, cryptonue, cryptostellar5, csanuragjain, ctf_sec, delfin454000, djxploit, durianSausage, erictee, fatherOfBlocks, gogo, i_got_hacked, immeas, joestakey, jumpdest7d, lukris02, martin, mcwildy, merlin, minhquanym, oyc_109, pashov, peanuts, pedr02b2, rbserver, rotcivegaf, rvierdiiev, sakman, saneryee, seyni, shark, slowmoses, tnevler, trustindistrust, w0Lfrum, yurahod, zaskoh
61.3462 USDC - $61.35
Issue | Risk | Instances | |
---|---|---|---|
1 | Use call() instead of transfer() | Low | 1 |
2 | Chainlink latestRoundData() could return stale prices | Low | 1 |
3 | Immutable state variables lack zero address checks | Low | 8 |
4 | require() /revert() statements should have descriptive reason strings | NC | 19 |
5 | Duplicated require() checks should be refactored to a modifier | NC | 6 |
6 | Named return variables not used anywhere in the functions | NC | 5 |
call()
instead of transfer()
:When transferring ETH, call()
should be used instead of transfer()
.
The transfer()
function only allows the recipient to use 2300 gas. If the recipient uses more than that, transfers will fail. In the future gas costs might change increasing the likelihood of that happening.
Keep in mind that call()
introduces the risk of reentrancy, but as the code follows the CEI pattern it should be fine.
Instances include:
File: contracts/utils/LineLib.sol Line 48
payable(receiver).transfer(amount);
Replace transfer()
calls with call()
, keep in mind to check whether the call was successful by validating the return value.
latestRoundData()
could return stale prices :The Chainlink latestRoundData()
function can sometimes return old prices if the current round is not complete or an old round is used, this can have a negative impact on the protocol lending/borrwing operations as the oracle is used to determine the assets prices when trying to trade or withdraw liquidity.
Instances include:
File: contracts/modules/oracle/Oracle.sol Line 22-32
function getLatestAnswer(address token) external returns (int) { ( /* uint80 roundID */, int price, /* uint80 startedAt */, /* uint80 timeStamp */, /* uint80 answeredInRound */ ) = registry.latestRoundData(token, Denominations.USD); return price; }
To avoid the risk of getting stale price from Chainlink oracle i recommend to use the latestRoundData
function with it's required checks.
The functions getLatestAnswer
should be modified as follow :
function getLatestAnswer(address token) external returns (int) { ( uint80 roundID , uint price, uint256 timestamp, uint80 answeredInRound ) = registry.latestRoundData(token, Denominations.USD); require(price > 0, "Invalid feed price"); require(answeredInRound >= roundID, "Stale price"); require(timestamp != 0, "Round not complete"); return price; }
Constructors should check the values written in an immutable state variables(address, uint, int) is not the zero value (address(0) or 0)
Instances include:
File: contracts/modules/credit/EscrowedLine.sol Line 17
escrow = IEscrow(_escrow);
File: contracts/modules/credit/LineOfCredit.sol Line 55
oracle = IOracle(oracle_);
File: contracts/modules/credit/LineOfCredit.sol Line 56
arbiter = arbiter_;
File: contracts/modules/credit/LineOfCredit.sol Line 57
borrower = borrower_;
File: contracts/modules/credit/SpigotedLine.sol Line 64
spigot = ISpigot(spigot_);
File: contracts/modules/credit/SpigotedLine.sol Line 66
swapTarget = swapTarget_;
File: contracts/modules/escrow/Escrow.sol Line 49
oracle = _oracle;
File: contracts/modules/escrow/Escrow.sol Line 50
borrower = _borrower;
Add non-zero address checks in the constructors for the instances aforementioned.
require()
/revert()
statements should have descriptive reason strings :Instances include:
File: contracts/modules/credit/LineOfCredit.sol 112 require(uint(status) >= uint( LineLib.STATUS.ACTIVE)); 241 require(interestRate.setRate(id, drate, frate)); 259 require(interestRate.setRate(id, drate, frate)); 326 require(amount <= credit.principal + credit.interestAccrued); File: contracts/modules/credit/SpigotedLine.sol 62 require(defaultRevenueSplit_ <= SpigotedLineLib.MAX_SPLIT); 143 require(amount <= unusedTokens[credit.token]); 160 require(msg.sender == borrower); 239 require(msg.sender == arbiter); File: contracts/utils/EscrowLib.sol 91 require(amount > 0); 105 require(msg.sender == ILineOfCredit(self.line).arbiter()); 161 require(amount > 0); 198 require(amount > 0); 216 require(msg.sender == self.line); File: contracts/utils/SpigotLib.sol 128 require(revenueContract != address(this)); 130 require(self.settings[revenueContract].transferOwnerFunction == bytes4(0)); 155 require(success); 180 require(newOwner != address(0)); 189 require(newOperator != address(0)); 201 require(newTreasury != address(0));
Add reason strings to the aforementioned require statements for better comprehension.
require()/revert()
checks should be refactored to a modifier :There are 6 instances of this issue:
File: contracts/utils/SpigotLib.sol 109 if(msg.sender != self.owner) { revert CallerAccessDenied(); } 126 if(msg.sender != self.owner) { revert CallerAccessDenied(); } 147 if(msg.sender != self.owner) { revert CallerAccessDenied(); } 168 if(msg.sender != self.owner) { revert CallerAccessDenied(); } 179 if(msg.sender != self.owner) { revert CallerAccessDenied(); } 209 if(msg.sender != self.owner) { revert CallerAccessDenied(); }
Those checks should be replaced by a onlyOwner
modifier as follow :
modifier onlyOwner(){ if(msg.sender != self.owner) { revert CallerAccessDenied(); } _; }
When Named return variable are declared they should be used inside the function instead of the return statement or if not they should be removed to avoid confusion.
Instances include:
File: contracts/utils/CreditLib.sol
returns (ILineOfCredit.Credit memory c, uint256 principal, uint256 interest)
returns(ILineOfCredit.Credit memory credit)
File: contracts/utils/SpigotLib.sol
Either use the named return variables inplace of the return statement or remove them.
#0 - c4-judge
2022-12-07T16:26:10Z
dmvt marked the issue as grade-b
π Selected for report: IllIllI
Also found by: 0x1f8b, 0xRajkumar, Awesome, Aymen0909, B2, Bnke0x0, Deivitto, Diana, JC, Metatron, Rahoz, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Saintcode_, TomJ, __141345__, ajtra, aphak5010, brgltd, c3phas, ch0bu, chrisdior4, cryptonue, durianSausage, emrekocak, erictee, exolorkistis, gogo, karanctf, lukris02, martin, me_na0mi, oyc_109, peanuts, rotcivegaf, saneryee, seyni, tnevler, zaskoh
49.2315 USDC - $49.23
Issue | Instances | |
---|---|---|
1 | Use unchecked blocks to save gas | 3 |
2 | x += y/x -= y costs more gas than x = x + y/x = x - y for state variables | 7 |
3 | ++i/i++ should be unchecked{++i} /unchecked{i++} when it is not possible for them to overflow, as in the case when used in for & while loops | 5 |
unchecked
blocks to save gas :Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isnβt possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block.
There are 3 instances of this issue:
File: contracts/modules/credit/SpigotedLine.sol Line 144
unusedTokens[credit.token] -= amount;
The above operation cannot underflow due to the check : require(amount <= unusedTokens[credit.token]); and should be modified as follows :
unchecked { unusedTokens[credit.token] -= amount; }
File: contracts/utils/EscrowLib.sol Line 164
self.deposited[token].amount -= amount;
The above operation cannot underflow due to the check : if(self.deposited[token].amount < amount) { revert InvalidCollateral(); } and should be modified as follows :
unchecked { self.deposited[token].amount -= amount; }
File: contracts/utils/EscrowLib.sol Line 202
self.deposited[token].amount -= amount;
The above operation cannot underflow due to the check : if(self.deposited[token].amount < amount) { revert InvalidCollateral(); } and should be modified as follows :
unchecked { self.deposited[token].amount -= amount; }
x += y/x -= y
costs more gas than x = x + y/x = x - y
for state variables :Using the addition operator instead of plus-equals saves 113 gas as explained here
There are 7 instances of this issue:
File: contracts/modules/credit/SpigotedLine.sol 122 unusedTokens[credit.token] -= repaid - newTokens; 125 unusedTokens[credit.token] += newTokens - repaid; 144 unusedTokens[credit.token] -= amount; 172 unusedTokens[targetToken] += newTokens; File: contracts/utils/EscrowLib.sol 96 self.deposited[token].amount += amount; 164 self.deposited[token].amount -= amount; 202 self.deposited[token].amount -= amount;
++i/i++
should be unchecked{++i}/unchecked{i++}
when it is not possible for them to overflow, as in the case when used in for & while loops :There are 5 instances of this issue:
File: contracts/modules/credit/LineOfCredit.sol 179 for (uint256 i; i < len; ++i) 203 for (uint256 i; i < len; ++i) 520 for (uint256 i; i <= lastSpot; ++i) File: contracts/utils/CreditListLib.sol 23 for(uint256 i; i < len; ++i) 51 for(uint i = 1; i < len; ++i)
#0 - c4-judge
2022-11-17T23:04:18Z
dmvt marked the issue as grade-b