Platform: Code4rena
Start Date: 01/07/2022
Pot Size: $75,000 USDC
Total HM: 17
Participants: 105
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 5
Id: 143
League: ETH
Rank: 28/105
Findings: 4
Award: $208.18
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xNineDec
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xdanial, 0xf15ers, Cheeezzyyyy, Chom, Franfran, GalloDaSballo, Green, IllIllI, Meera, Ruhum, bardamu, cccz, codexploder, defsec, hake, hansfriese, horsefacts, hubble, hyh, jonatascm, kebabsec, oyc_109, pashov, rbserver, simon135, tabish, tintin, zzzitron
14.8726 USDC - $14.87
In JBChainlinkV3PriceFeed.sol#L43-L44, we are using latestRoundData
, but there are no validations that the data is not stale.
The current code is:
// Get the latest round information. Only need the price is needed. (, int256 _price, , , ) = feed.latestRoundData();
But is missing the checks to validate the data is stale
// Get the latest round information. Only need the price is needed. - (, int256 _price, , , ) = feed.latestRoundData(); + (uint80 round, int256 _price, , uint256 latestTimestamp, answeredInRound) = feed.latestRoundData(); require(_price> 0, "Chainlink price <= 0"); require(latestTimestamp != 0, "Incomplete round"); require(answeredInRound >= round, "Stale price");
This could affect in all the logic, including funds.
#0 - berndartmueller
2022-07-08T20:09:42Z
Out of scope
#1 - 0x1f8b
2022-07-08T22:26:28Z
Out of scope
@berndartmueller
note: Issues with the oracle price resolvers are also in scope.
#2 - berndartmueller
2022-07-08T22:32:08Z
For the judge: Please decide if in scope as the readme clearly does mention this to be in scope. Thanks!
#3 - drgorillamd
2022-07-12T15:45:41Z
Duplicate of #138
🌟 Selected for report: horsefacts
Also found by: 0x1f8b, 0x29A, 0x52, 0xf15ers, AlleyCat, Ch_301, Chom, Franfran, IllIllI, Kaiziron, Limbooo, Meera, Ruhum, Sm4rty, apostle0x01, berndartmueller, cccz, cloudjunky, codexploder, cryptphi, delfin454000, durianSausage, fatherOfBlocks, hake, hansfriese, hyh, jonatascm, m_Rassska, oyc_109, peritoflores, rajatbeladiya, rbserver, svskaushik, zzzitron
3.4075 USDC - $3.41
JBERC20PaymentTerminal._beforeTransferTo
is not compatible with Tether (USDT) implementation
Some tokens do not implement the ERC20 standard properly but are still accepted by most code that accepts ERC20 tokens. For example Tether (USDT or CVX)'s approve()
function will revert if the current approval is not zero, to protect against front-running changes of approvals.
function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) { // To change the approve amount you first have to reduce the addresses` // allowance to zero by calling `approve(_spender, 0)` if it is not // already 0 to mitigate the race condition described here: // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); allowed[msg.sender][_spender] = _value; Approval(msg.sender, _spender, _value); }
The code as currently implemented does not handle these sorts of tokens properly, which would prevent USDT, from being used by this project.
Reference:
NOTE: To prevent attack vectors like the one described here and discussed here, clients SHOULD make sure to create user interfaces in such a way that they set the allowance first to 0 before setting it to another value for the same spender. THOUGH The contract itself shouldn’t enforce it, to allow backwards compatibility with contracts deployed before
Affected source code:
0
first.#0 - mejango
2022-07-12T18:02:07Z
dup #101
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xDjango, 0xNazgul, 0xNineDec, 0xdanial, 0xf15ers, Bnke0x0, Ch_301, Chandr, Chom, Funen, GimelSec, Hawkeye, JC, Kaiziron, Lambda, Meera, MiloTruck, Noah3o6, Picodes, ReyAdmirado, Rohan16, Sm4rty, TerrierLover, TomJ, Waze, _Adam, __141345__, asutorufos, aysha, berndartmueller, brgltd, cccz, codexploder, defsec, delfin454000, djxploit, durianSausage, fatherOfBlocks, hake, horsefacts, hubble, jayfromthe13th, joestakey, jonatascm, m_Rassska, oyc_109, pashov, rajatbeladiya, rbserver, robee, sach1r0, sahar, samruna, simon135, svskaushik, zzzitron
109.1272 USDC - $109.13
Some used packages are out of date, it is good practice to use the latest version of these packages:
"@openzeppelin/contracts" : "^4.5.0-rc.0",
=> v4.7.0
"@chainlink/contracts" : "^0.1.6",
=> v0.4.1
The pragma version used is:
pragma solidity 0.8.6;
But recently solidity released a new version with important Bugfixes:
The first one is related to ABI-encoding nested arrays directly from calldata. You can find more information here.
The second bug is triggered in certain inheritance structures and can cause a memory pointer to be interpreted as a calldata pointer or vice-versa. We also have a dedicated blog post about this bug.
Apart from these, there are several minor bug fixes and improvements.
The minimum required version should be 0.8.14
Check for address(0)
during constructor
, otherwise it could lead to bad initialization, bad implementations, or bad admin changes.
Affected source code:
address(0)
:
It's possible to lose the ownership under specific circumstances.
Because an 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:
JBSingleTokenPaymentTerminal
The decimalsForToken and currencyForToken methods return the value even though the token is not accepted, it should be:
function decimalsForToken(address _token) external view override returns (uint256) { + if (_token != token) revert TOKEN_NOT_ACCEPTED(); // Check `acceptsToken` return decimals; } function currencyForToken(address _token) external view override returns (uint256) { + if (_token != token) revert TOKEN_NOT_ACCEPTED(); // Check `acceptsToken` return currency; }
The project contains packages that urgently need to be updated because they contain important vulnerabilities.
npm audit
:
69 vulnerabilities (21 moderate, **45 high**, 3 critical) To address issues that do not require attention, run: npm audit fix
enum
instead of constant
enum
checks the accepted range of values, in constants it does not. So the user or developer may set a value out of range.
Affected source code:
The user may think that they are approving for a particular project _projectId
but they are actually approving for the account, so it is better to remove the argument. All ERC20 methods are affected by this unusable argument.
Affected source code:
The following code doesn't check the result of the ERC20 calls. ERC20 standard specify that the token can return false if these calls fails, so it's mandatory to check the result of these ERC20 methods.
Reference:
NOTES: The following specifications use syntax from Solidity 0.4.17 (or above). Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned!
Affected source code for transfer
and transferFrom
:
Affected source code for approve
:
With a very low _baseFundingCycle.duration
, such as 1, the loop may overgas with some values in _mustStartAtOrAfter
and _baseFundingCycle
in JBFundingCycleStore.sol#L686
Similarly in JBFundingCycleStore.sol#L755 certain configurations could produce a denial of service due to integer underflow..
uint8
for decimalsSince the ERC20 sets decimals as a byte, the interface and the contract should use it with that type.
Affected source code:
🌟 Selected for report: 0xA5DF
Also found by: 0v3rf10w, 0x09GTO, 0x1f8b, 0x29A, 0xDjango, 0xKitsune, 0xNazgul, 0xdanial, 0xf15ers, Aymen0909, Bnke0x0, Ch_301, Cheeezzyyyy, Chom, ElKu, Funen, Hawkeye, IllIllI, JC, JohnSmith, Kaiziron, Lambda, Limbooo, Meera, Metatron, MiloTruck, Noah3o6, Picodes, Randyyy, RedOneN, ReyAdmirado, Rohan16, Saintcode_, Sm4rty, TomJ, Tomio, Tutturu, UnusualTurtle, Waze, _Adam, __141345__, ajtra, apostle0x01, asutorufos, brgltd, c3phas, cRat1st0s, codexploder, defsec, delfin454000, djxploit, durianSausage, exd0tpy, fatherOfBlocks, hake, horsefacts, ignacio, jayfromthe13th, joestakey, jonatascm, kaden, kebabsec, m_Rassska, mektigboy, mrpathfindr, oyc_109, rajatbeladiya, rbserver, rfa, robee, sach1r0, sashik_eth, simon135
80.7665 USDC - $80.77
The following structs could be optimized moving the position of certains values in order to save slot storages:
struct JBSplit { bool preferClaimed; bool preferAddToBalance; + address payable beneficiary; // < - Move close to bool values uint256 percent; uint256 projectId; - address payable beneficiary; uint256 lockedUntil; IJBSplitAllocator allocator; }
++i
costs less gas compared to i++
or i += 1
++i
costs less gas compared to i++
or i += 1
for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.
i++
increments i
and returns the initial value of i
. Which means:
uint i = 1; i++; // == 1 but i == 2
But ++i
returns the actual incremented value:
uint i = 1; ++i; // == 2 and i == 2 too, so no need for a temporary variable
In the first case, the compiler has to create a temporary variable (when used) for returning 1
instead of 2
I suggest using ++i
instead of i++
to increment the value of an uint variable. Same thing for --i
and i--
Affected source code:
If a variable is not set/initialized, the default value is assumed (0, false
, 0x0 ... depending on the data type). You are simply wasting gas if you directly initialize it with its default value.
Affected source code:
Change JBOperatorStore.hasPermissions as follows to avoid redundant storage access:
function hasPermissions(address _operator, address _account, uint256 _domain, uint256[] calldata _permissionIndexes ) external view override returns (bool) { + uint256 permissions = permissionsOf[_operator][_account][_domain]; for (uint256 _i; _i < _permissionIndexes.length; ++_i) { uint256 _permissionIndex = _permissionIndexes[_i]; if (_permissionIndex > 255) revert PERMISSION_INDEX_OUT_OF_BOUNDS(); - if (((permissionsOf[_operator][_account][_domain] >> _permissionIndex) & 1) == 0) + if (((permissions >> _permissionIndex) & 1) == 0) return false; } return true; }
Add break
inside JBSplitsStore.sol#L220 loop condition:
if ( _splits[_j].percent == _currentSplits[_i].percent && _splits[_j].beneficiary == _currentSplits[_i].beneficiary && _splits[_j].allocator == _currentSplits[_i].allocator && _splits[_j].projectId == _currentSplits[_i].projectId && // Allow lock extention. _splits[_j].lockedUntil >= _currentSplits[_i].lockedUntil - ) _includesLocked = true; + ) { _includesLocked = true; break; }
immutable
It's possible to avoid storage access a save gas using immutable
keyword for the following variables:
It's also better to remove the initial values, because they will be set during the constructor.
Affected source code:
The logic to check that the address its a contract can be eliminated since a call to supportsInterface
is then made, which would give the same error code.
- address _ballot = address(_data.ballot); - uint32 _size; - assembly { - _size := extcodesize(_ballot) // No contract at the address ? - } - if (_size == 0) revert INVALID_BALLOT(); try _data.ballot.supportsInterface(type(IJBFundingCycleBallot).interfaceId) returns (bool _supports) { if(!_supports) revert INVALID_BALLOT(); // Contract exists at the address but with the wrong interface } catch { revert INVALID_BALLOT(); // No ERC165 support }
Use delete
instead of set to default value (false
or 0
).
Here you can see an example of this optimization where 5 gas is saved:
contract A { mapping(uint256 => uint256) public balanceOf; constructor () { balanceOf[1] = 2; } function test1() public { balanceOf[1] = 0; // 21478 gas } } contract B { mapping(uint256 => uint256) public balanceOf; constructor () { balanceOf[1] = 2; } function test1() public { delete balanceOf[1]; // 21473 Gas } }
Affected source code:
Use scientific notation (e.g. 10e17
) rather than exponentiation (e.g. 10**18
)
10**18
=> 10e17
: