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: 10/105
Findings: 5
Award: $1,303.56
π Selected for report: 1
π 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
The _price
provided by chainlink AggregatorV3 could be a negative, if that happend the cast of the _price
goes high, in example, cast -1
to uint256 was 2**256 - 1
return uint256(_price).adjustDecimals(_feedDecimals, _decimals);
Manual Review
Use a SafeCast library of openzeppelin toUint256(int256 value) or check the _price
with a require before cast it: require(_price >= 0, "_price must be positive");
#0 - jack-the-pug
2022-08-07T04:23:37Z
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
In the contract JBERC20PaymentTerminal
, the return values of ERC20 transfer and transferFrom are not checked to be true, which could be false if the transferred tokens are not ERC20-compliant. In that case, the transfer fails without being noticed by the calling contract.
Out of scope but also found in
JBETHERC20SplitsPayer.sol
L256, L301, L348, L384, L534 andJBETHERC20ProjectPayer.sol
L271, L301, L315, L384, L534
With the current implementation of _transferFrom
, it could be that trasnferFrom
return false and not revert when the sender does not have enough token to transfer (both for transfer and transferFrom).
Manual Review
Use the SafeERC20 library implementation from OpenZeppelin and call safeTransfer or safeTransferFrom when transferring ERC20 tokens.
#0 - mejango
2022-07-12T18:39:32Z
dup #281
https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L415-L448 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L788-L900 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L981-L1174 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBETHPaymentTerminal.sol#L63-L79 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBERC20PaymentTerminal.sol#L73-L89
In the contract JBPayoutRedemptionPaymentTerminal
, the function distributePayoutsOf
calls the internal function _distributePayoutsOf
and this internal function perfoms a loop where is using the function _distributeToPayoutSplitsOf
In these functions there are a _transferFrom
what:
JBETHPaymentTerminal
using a Address.sendValue(_to, _amount)
JBERC20PaymentTerminal
using a IERC20(token).transfer(_to, _amount)
with a ERC777
as tokenBoth give back the control to the msg.sender
(_to
variable) creating a reentrancy attack vector
Also could end with a lot of bad calculation because is using uncheckeds statements and function _distributePayoutsOf
its no respecting the checks, effects, interactions
pattern
Craft a contract to call function distributePayoutsOf
, on receive ether reentrant to function distributePayoutsOf
or use a ERC777
callback.
Manual Review
Add a reentrancyGuard as you do on JBSingleTokenPaymentTerminalStore.sol
;
You have already import the ReentrancyGuard on JBPayoutRedemptionPaymentTerminal.sol#L5 but you are not using it.
My recommendation is to add nonReentrant
modifier on function distributePayoutsOf
#0 - drgorillamd
2022-07-12T18:28:59Z
Duplicate of #35
#1 - jack-the-pug
2022-07-24T01:43:16Z
Lack of clear path to exploit it, but it dose seems like _distributeToPayoutSplitsOf
can be used to reenter distributePayoutsOf
; it requires the attacker to be one of the project's splits beneficiaries, though.
_transferFrom( address(this), _split.beneficiary != address(0) ? _split.beneficiary : payable(msg.sender), _netPayoutAmount );
π 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
89.271 USDC - $89.27
https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L816 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L668 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L681 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L743 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L785 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L859
The JBController contract performs many unsafe casts uint256
to int256
and int256
to uint256
In example:
-1
(int256) to uint256 was 2**256 - 1
2**255
(uint256) to int256 was - 2**255
int256
to uint256
:
uint256
to int256
:
int256(_tokenCount);
int256(beneficiaryTokenCount);
int256(_tokenCount);
_processedTokenTrackerOf[_projectId] = int256(tokenStore.totalSupplyOf(_projectId));
_processedTokenTrackerOf[_projectId] = int256(_totalTokens + tokenCount);
Note: in the L1076 and L1077 there are two more casts but in the L1075 check the cast
Review
Use a SafeCast library of openzeppelin toUint256(int256 value)
and toInt256(uint256 value)
or check the number before cast it
#0 - mejango
2022-07-12T20:23:08Z
acknowledged here https://info.juicebox.money/dev/learn/risks#large-number-risk
π 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
38.2308 USDC - $38.23
++i
costs less gas compared to i++
or i += 1
and use unchecked
for boost save gas++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.
Also using unchecked arithmetic will cost less gass because it wouldnt run a check for under/overflow and revert if it is detected.
Take in mind you shoul only use unchecked
when its safe to increment without overflow, like in loops iterator.
Recommendation:
Replace this lines
JBDirectory.sol:139: for (uint256 _i; _i < _terminalsOf[_projectId].length; _i++) { JBDirectory.sol:167: for (uint256 _i; _i < _terminalsOf[_projectId].length; _i++) JBDirectory.sol:275: for (uint256 _i; _i < _terminals.length; _i++) JBDirectory.sol:276: for (uint256 _j = _i + 1; _j < _terminals.length; _j++) JBFundingCycleStore.sol:724: for (uint256 i = 0; i < _discountMultiple; i++) { JBSingleTokenPaymentTerminalStore.sol:862: for (uint256 _i = 0; _i < _terminals.length; _i++) JBSplitsStore.sol:204: for (uint256 _i = 0; _i < _currentSplits.length; _i++) { JBSplitsStore.sol:211: for (uint256 _j = 0; _j < _splits.length; _j++) { JBSplitsStore.sol:229: for (uint256 _i = 0; _i < _splits.length; _i++) { JBSplitsStore.sol:304: for (uint256 _i = 0; _i < _splitCount; _i++) { JBOperatorStore.sol:85: for (uint256 _i = 0; _i < _permissionIndexes.length; _i++) { JBOperatorStore.sol:135: for (uint256 _i = 0; _i < _operatorData.length; _i++) { JBOperatorStore.sol:165: for (uint256 _i = 0; _i < _indexes.length; _i++) { JBController.sol:913: for (uint256 _i = 0; _i < _splits.length; _i++) { JBController.sol:1014: for (uint256 _i; _i < _fundAccessConstraints.length; _i++) {
and use this pattern:
for (uint256 _i; _i < length;) { // code here unchecked { ++_i; } }
unchecked
arithmeticThe default βcheckedβ behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked block will save gas
Example
JBProjects.sol:137: projectId = ++count;
This could be
JBProjects.sol:137: unchecked { projectId = ++count; }
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
In particular, in for loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is high.
diff --git a/contracts/JBDirectory.sol b/contracts/JBDirectory.sol index e6d5a93..72a3b8c 100644 --- a/contracts/JBDirectory.sol +++ b/contracts/JBDirectory.sol @@ -129,15 +129,17 @@ contract JBDirectory is IJBDirectory, JBOperatable, Ownable { override returns (IJBPaymentTerminal) { + IJBPaymentTerminal _terminalToken = _primaryTerminalOf[_projectId][_token]; // If a primary terminal for the token was specifically set and its one of the project's terminals, return it. if ( - _primaryTerminalOf[_projectId][_token] != IJBPaymentTerminal(address(0)) && - isTerminalOf(_projectId, _primaryTerminalOf[_projectId][_token]) - ) return _primaryTerminalOf[_projectId][_token]; + _terminalToken != IJBPaymentTerminal(address(0)) && + isTerminalOf(_projectId, _terminalToken) + ) return _terminalToken; // Return the first terminal which accepts the specified token. - for (uint256 _i; _i < _terminalsOf[_projectId].length; _i++) { - IJBPaymentTerminal _terminal = _terminalsOf[_projectId][_i]; + IJBPaymentTerminal[] memory _terminals = _terminalsOf[_projectId]; + for (uint256 _i; _i < _terminals.length; _i++) { + IJBPaymentTerminal _terminal = _terminals[_i]; if (_terminal.acceptsToken(_token, _projectId)) return _terminal; } @@ -164,8 +166,9 @@ contract JBDirectory is IJBDirectory, JBOperatable, Ownable { override returns (bool) { - for (uint256 _i; _i < _terminalsOf[_projectId].length; _i++) - if (_terminalsOf[_projectId][_i] == _terminal) return true; + IJBPaymentTerminal[] memory _terminals = _terminalsOf[_projectId]; + for (uint256 _i; _i < _terminals.length; _i++) + if (_terminals[_i] == _terminal) return true; return false; } @@ -270,10 +273,11 @@ contract JBDirectory is IJBDirectory, JBOperatable, Ownable { // Delete the stored terminals for the project. _terminalsOf[_projectId] = _terminals; + uint256 _length = _terminals.length; // Make sure duplicates were not added. - if (_terminals.length > 1) - for (uint256 _i; _i < _terminals.length; _i++) - for (uint256 _j = _i + 1; _j < _terminals.length; _j++) + if (_length > 1) + for (uint256 _i; _i < _length; _i++) + for (uint256 _j = _i + 1; _j < _length; _j++) if (_terminals[_i] == _terminals[_j]) revert DUPLICATE_TERMINALS(); emit SetTerminals(_projectId, _terminals, msg.sender);