Platform: Code4rena
Start Date: 12/08/2022
Pot Size: $50,000 USDC
Total HM: 15
Participants: 120
Period: 5 days
Judge: Justin Goro
Total Solo HM: 6
Id: 153
League: ETH
Rank: 17/120
Findings: 3
Award: $323.44
🌟 Selected for report: 0
🚀 Solo Findings: 0
The specified timelock address in FraxlendPair.sol
serves the sole purpose of providing a timelock for updating the feeToProtocolRate
. This variable controls the amount of interest that is directed to the protocol instead of the lenders with a maximum fee of 50%
(per the comments). Since the timelock contract can be arbitrarily set without a timelock of its own, the fee can be changed without a timelock by extension.
The changeFee()
function requires that the caller is the specified timelock contract:
function changeFee(uint32 _newFee) external whenNotPaused { if (msg.sender != TIME_LOCK_ADDRESS) revert OnlyTimeLock(); ... }
However, the TIME_LOCK_ADDRESS
can be set at any time by the owner:
function setTimeLock(address _newAddress) external onlyOwner { emit SetTimeLock(TIME_LOCK_ADDRESS, _newAddress); TIME_LOCK_ADDRESS = _newAddress; }
Manual review.
In order to maintain the flexibility of changing the timelock address, I recommend adding the same check to setTimeLock()
that requires that the caller is the original timelock contract. In other words, setting a new timelock contract requires a waiting period defined by the original timelock contract.
#0 - DrakeEvans
2022-09-06T13:35:38Z
Duplicate of #129
🌟 Selected for report: 0x1f8b
Also found by: 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xmatt, 0xsolstars, Aymen0909, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, EthLedger, Funen, IllIllI, JC, Junnon, Lambda, LeoS, MiloTruck, Noah3o6, PaludoX0, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, SaharAP, Sm4rty, SooYa, The_GUILD, TomJ, Waze, Yiko, _Adam, __141345__, a12jmx, ak1, asutorufos, auditor0517, ayeslick, ballx, beelzebufo, berndartmueller, bin2chen, brgltd, c3phas, cRat1st0s, cccz, cryptonue, cryptphi, d3e4, delfin454000, dipp, djxploit, durianSausage, dy, erictee, fatherOfBlocks, gogo, gzeon, hyh, ignacio, kyteg, ladboy233, medikko, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, ret2basic, robee, sach1r0, simon135, sryysryy, tabish, yac, yash90, zzzitron
52.6429 USDC - $52.64
Finding | Instances | |
---|---|---|
[L-01] | Floating Pragma | 1 |
[L-02] | approve has been deprecated | 4 |
[L-03] | abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() | 1 |
[L-04] | Missing zero address check might lead to redeployment | 1 |
Finding | Instances | |
---|---|---|
[N-01] | The use of magic numbers is not recommended | 3 |
[N-02] | Unindexed parameters in events | 4 |
Contract | Total Instances | Total Findings | Low Findings | Low Instances | NC Findings | NC Instances |
---|---|---|---|---|---|---|
FraxlendPairCore.sol | 9 | 4 | 3 | 6 | 1 | 3 |
FraxlendPairDeployer.sol | 4 | 2 | 1 | 1 | 1 | 3 |
FraxlendWhitelist.sol | 1 | 1 | 0 | 0 | 1 | 1 |
A floating pragma might result in contract being tested/deployed with different compiler versions possibly leading to unexpected behaviour. 1 instance of this issue has been found:
[L-01] FraxlendPairCore.sol#L2-L3
pragma solidity ^0.8.15;
approve
has been deprecatedPlease use safeIncreaseAllowance
and safeDecreaseAllowance
instead
4 instances of this issue have been found:
[L-02] FraxlendPairCore.sol#L1184-L1185
_collateralContract.approve(_swapperAddress, _collateralToSwap);
[L-02b] FraxlendPairCore.sol#L1103-L1104
_assetContract.approve(_swapperAddress, _borrowAmount);
[L-02c] FraxlendPairCore.sol#L1184-L1185
_collateralContract.approve(_swapperAddress, _collateralToSwap);
[L-02d] FraxlendPairCore.sol#L633-L634
if (allowed != type(uint256).max) _approve(_owner, msg.sender, allowed - _shares);
abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such as keccak256()
Use abi.encode()
instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456)
=> 0x123456
=> abi.encodePacked(0x1,0x23456)
, but abi.encode(0x123,0x456)
=> 0x0...1230...456)
. If there is only one argument to abi.encodePacked()
it can often be cast to bytes()
or bytes32()
instead.
1 instance of this issue has been found:
[L-03] FraxlendPairDeployer.sol#L204-L205
bytes32 salt = keccak256(abi.encodePacked(_saltSeed, _configData));
If variable is accidentally set to zero the contract will have to be redeployed. 1 instance of this issue has been found:
[L-04] FraxlendPairCore.sol#L171-L176
DEPLOYER_ADDRESS = msg.sender; CIRCUIT_BREAKER_ADDRESS = _circuitBreaker; COMPTROLLER_ADDRESS = _comptrollerAddress; TIME_LOCK_ADDRESS = _timeLockAddress; FRAXLEND_WHITELIST_ADDRESS = _fraxlendWhitelistAddress; }
Consider setting constant numbers as a constant
variable for better readability and clarity.
3 instances of this issue have been found:
[N-01] FraxlendPairDeployer.sol#L171-L172
bytes memory _firstHalf = BytesLib.slice(_creationCode, 0, 13000);
[N-01b] FraxlendPairDeployer.sol#L173-L174
if (_creationCode.length > 13000) {
[N-01c] FraxlendPairDeployer.sol#L174-L175
bytes memory _secondHalf = BytesLib.slice(_creationCode, 13000, _creationCode.length - 13000);
Events should index all first three parameters. 4 instances of this issue have been found:
[N-02] FraxlendPairCore.sol#L1143-L1149
event RepayAssetWithCollateral( address indexed _borrower, address _swapperAddress, uint256 _collateralToSwap, uint256 _amountAssetOut, uint256 _sharesRepaid );
[N-02b] FraxlendPairCore.sol#L1045-L1052
event LeveragedPosition( address indexed _borrower, address _swapperAddress, uint256 _borrowAmount, uint256 _borrowShares, uint256 _initialCollateralAmount, uint256 _amountCollateralOut );
[N-02c] FraxlendWhitelist.sol#L45-L46
event SetOracleWhitelist(address indexed _address, bool _bool);
[N-02d] FraxlendPairCore.sol#L897-L904
event Liquidate( address indexed _borrower, uint256 _collateralForLiquidator, uint256 _sharesToLiquidate, uint256 _amountLiquidatorToRepay, uint256 _sharesToAdjust, uint256 _amountToAdjust );
#0 - gititGoro
2022-10-05T23:14:48Z
Extra points for high quality report.
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xA5DF, 0xDjango, 0xNazgul, 0xSmartContract, 0xackermann, 0xbepresent, 0xc0ffEE, 0xkatana, 2997ms, Amithuddar, Aymen0909, Bnke0x0, Chinmay, Chom, CodingNameKiki, Deivitto, Diraco, Dravee, ElKu, EthLedger, Fitraldys, Funen, IgnacioB, JC, Junnon, Lambda, LeoS, Metatron, MiloTruck, Noah3o6, NoamYakov, PaludoX0, Randyyy, ReyAdmirado, Rohan16, Rolezn, Ruhum, SaharAP, Sm4rty, SooYa, TomJ, Tomio, Waze, Yiko, _Adam, __141345__, a12jmx, ajtra, ak1, asutorufos, ballx, brgltd, c3phas, cRat1st0s, carlitox477, chrisdior4, d3e4, delfin454000, dharma09, djxploit, durianSausage, erictee, fatherOfBlocks, find_a_bug, flyx, francoHacker, gerdusx, gogo, gzeon, hakerbaya, ignacio, jag, kyteg, ladboy233, ltyu, m_Rassska, medikko, mics, mrpathfindr, newfork01, nxrblsrpr, oyc_109, pfapostol, rbserver, reassor, ret2basic, robee, sach1r0, saian, simon135, sryysryy, zeesaw
21.2525 USDC - $21.25
Finding | Instances | |
---|---|---|
[G-01] | Variable should be made a constant | 3 |
[G-02] | Using prefix(++i ) instead of postfix (i++ ) saves gas | 5 |
[G-03] | Setting variable to default value is redundant | 9 |
[G-04] | for loop increments should be unchecked{} if overflow is not possible | 5 |
[G-05] | array.length should be cached in for loop | 7 |
[G-06] | Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate | 2 |
[G-07] | Avoid using bool for storage | 5 |
[G-08] | Function could accept array for bool | 3 |
Contract | Instances | Gas Ops |
---|---|---|
FraxlendWhitelist.sol | 18 | 6 |
FraxlendPairCore.sol | 11 | 6 |
FraxlendPair.sol | 6 | 3 |
FraxlendPairDeployer.sol | 4 | 2 |
constant
A variable that is not changed should be made constant
in order to save gas.
3 instances of this issue have been found:
[G-01] FraxlendPairDeployer.sol#L49-L51
uint256 public DEFAULT_MAX_LTV = 75000; // 75% with 1e5 precision uint256 public GLOBAL_MAX_LTV = 1e8; // 1000x (100,000%) with 1e5 precision, protects from rounding errors in LTV calc uint256 public DEFAULT_LIQ_FEE = 10000; // 10% with 1e5 precision
[G-01b] FraxlendPairDeployer.sol#L57-L59
address public CIRCUIT_BREAKER_ADDRESS; address public COMPTROLLER_ADDRESS;
[G-01c] FraxlendPairCore.sol#L86-L87
address public TIME_LOCK_ADDRESS;
++i
) instead of postfix (i++
) saves gasIt saves 6 gas per iteration. 5 instances of this issue have been found:
[G-02] FraxlendWhitelist.sol#L81-L82
for (uint256 i = 0; i < _addresses.length; i++) {
[G-02b] FraxlendWhitelist.sol#L66-L67
for (uint256 i = 0; i < _addresses.length; i++) {
[G-02c] FraxlendWhitelist.sol#L51-L52
for (uint256 i = 0; i < _addresses.length; i++) {
[G-02d] FraxlendPair.sol#L289-L290
for (uint256 i = 0; i < _lenders.length; i++) {
[G-02e] FraxlendPair.sol#L308-L309
for (uint256 i = 0; i < _borrowers.length; i++) {
Variables are by default set to 0 and is a waist of gas if variable sits in storage.
Examples to avoid:
uint i = 0
bool success = false
9 instances of this issue have been found:
[G-03] FraxlendWhitelist.sol#L81-L82
for (uint256 i = 0; i < _addresses.length; i++) {
[G-03b] FraxlendWhitelist.sol#L66-L67
for (uint256 i = 0; i < _addresses.length; i++) {
[G-03c] FraxlendWhitelist.sol#L51-L52
for (uint256 i = 0; i < _addresses.length; i++) {
[G-03d] FraxlendPairDeployer.sol#L127-L128
for (i = 0; i < _lengthOfArray; ) {
[G-03e] FraxlendPair.sol#L289-L290
for (uint256 i = 0; i < _lenders.length; i++) {
[G-03f] FraxlendPairDeployer.sol#L402-L403
for (uint256 i = 0; i < _lengthOfArray; ) {
[G-03g] FraxlendPair.sol#L308-L309
for (uint256 i = 0; i < _borrowers.length; i++) {
[G-03h] FraxlendPairCore.sol#L270-L271
for (uint256 i = 0; i < _approvedLenders.length; ++i) {
[G-03i] FraxlendPairCore.sol#L265-L266
for (uint256 i = 0; i < _approvedBorrowers.length; ++i) {
for
loop increments should be unchecked{}
if overflow is not possibleFrom Solidity 0.8.0
onwards using the unchecked
keyword saves 30 to 40 gas per loop.
Example:
uint length = array.length; for (uint i; i < length;){ uncheck { ++i } }
5 instances of this issue have been found:
[G-04] FraxlendWhitelist.sol#L81-L82
for (uint256 i = 0; i < _addresses.length; i++) {
[G-04b] FraxlendWhitelist.sol#L66-L67
for (uint256 i = 0; i < _addresses.length; i++) {
[G-04c] FraxlendWhitelist.sol#L51-L52
for (uint256 i = 0; i < _addresses.length; i++) {
[G-04d] FraxlendPairCore.sol#L270-L271
for (uint256 i = 0; i < _approvedLenders.length; ++i) {
[G-04e] FraxlendPairCore.sol#L265-L266
for (uint256 i = 0; i < _approvedBorrowers.length; ++i) {
array.length
should be cached in for
loopCaching the length array would save gas on each iteration by not having to read from memory or storage multiple times. Example:
uint length = array.length; for (uint i; i < length;){ uncheck { ++i } }
7 instances of this issue have been found:
[G-05] FraxlendWhitelist.sol#L81-L82
for (uint256 i = 0; i < _addresses.length; i++) {
[G-05b] FraxlendWhitelist.sol#L66-L67
for (uint256 i = 0; i < _addresses.length; i++) {
[G-05c] FraxlendWhitelist.sol#L51-L52
for (uint256 i = 0; i < _addresses.length; i++) {
[G-05d] FraxlendPair.sol#L289-L290
for (uint256 i = 0; i < _lenders.length; i++) {
[G-05e] FraxlendPair.sol#L308-L309
for (uint256 i = 0; i < _borrowers.length; i++) {
[G-05f] FraxlendPairCore.sol#L270-L271
for (uint256 i = 0; i < _approvedLenders.length; ++i) {
[G-05g] FraxlendPairCore.sol#L265-L266
for (uint256 i = 0; i < _approvedBorrowers.length; ++i) {
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operatio 2 instances of this issue have been found:
[G-06] FraxlendPairCore.sol#L132-L137
// Internal Whitelists bool public immutable borrowerWhitelistActive; mapping(address => bool) public approvedBorrowers; bool public immutable lenderWhitelistActive; mapping(address => bool) public approvedLenders;
[G-06b] FraxlendPairCore.sol#L125-L129
// User Level Accounting /// @notice Stores the balance of collateral for each user mapping(address => uint256) public userCollateralBalance; // amount of collateral each user is backed /// @notice Stores the balance of borrow shares for each user mapping(address => uint256) public userBorrowShares
bool
for storage"Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled." - Source: OZ.
Instead use uint256(1)
and uint256(2)
for true and false.
5 instances of this issue have been found:
[G-07] FraxlendPairCore.sol#L136-L137
bool public immutable lenderWhitelistActive;
[G-07b] FraxlendPairCore.sol#L133-L134
bool public immutable borrowerWhitelistActive;
[G-07c] FraxlendWhitelist.sol#L32-L33
mapping(address => bool) public oracleContractWhitelist;
[G-07d] FraxlendWhitelist.sol#L35-L36
mapping(address => bool) public rateContractWhitelist;
[G-07e] FraxlendWhitelist.sol#L38-L39
mapping(address => bool) public fraxlendDeployerWhitelist;
bool
If function accepted an array for bool
it would be more gas efficient because it would be able to set contract for both to true and false in the same call.
3 instances of this issue have been found:
[G-08] FraxlendWhitelist.sol#L80-L85
function setFraxlendDeployerWhitelist(address[] calldata _addresses, bool _bool) external onlyOwner { for (uint256 i = 0; i < _addresses.length; i++) { fraxlendDeployerWhitelist[_addresses[i]] = _bool; emit SetFraxlendDeployerWhitelist(_addresses[i], _bool); } }
[G-08b] FraxlendWhitelist.sol#L65-L70
function setRateContractWhitelist(address[] calldata _addresses, bool _bool) external onlyOwner { for (uint256 i = 0; i < _addresses.length; i++) { rateContractWhitelist[_addresses[i]] = _bool; emit SetRateContractWhitelist(_addresses[i], _bool); } }
[G-08c] FraxlendWhitelist.sol#L50-L55
function setOracleContractWhitelist(address[] calldata _addresses, bool _bool) external onlyOwner { for (uint256 i = 0; i < _addresses.length; i++) { oracleContractWhitelist[_addresses[i]] = _bool; emit SetOracleWhitelist(_addresses[i], _bool); } }