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: 3/105
Findings: 5
Award: $6,473.84
π Selected for report: 2
π Solo Findings: 1
π 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
MED - the function of the protocol could be impacted
// JBPrices::priceFor at line 69 calls _feed.currentPrice 57 function priceFor( 58 uint256 _currency, 59 uint256 _base, 60 uint256 _decimals 61 ) external view override returns (uint256) { // ... 68 // If it exists, return the price. 69: if (_feed != IJBPriceFeed(address(0))) return _feed.currentPrice(_decimals);
// JBChainlinkV3PriceFeed::currentPrice calls latestRoundData and uses only the price 42 function currentPrice(uint256 _decimals) external view override returns (uint256) { 43 // Get the latest round information. Only need the price is needed. 44: (, int256 _price, , , ) = feed.latestRoundData(); 45 46 // Get a reference to the number of decimals the feed uses. 47 uint256 _feedDecimals = feed.decimals(); 48 49 // Return the price, adjusted to the target decimals. 50 return uint256(_price).adjustDecimals(_feedDecimals, _decimals);
The JBPrices
contract fetches price information from chainlink and the information is used in JBSingleTokenPaymentTerminalStore
, to determine how much token should be issued, etc.
There are two issues with the line 44 of JBChainlinkV3PriceFeed
The returned information might be stale There are not checks on roundID nor timeStamp, resulting in the possibility to use stale price information.
It does not checked whether the returned value is greater than zero
The returned price is the type of int256
, so for some reason a negative value is passed as the answer, it will give a wrong value on the line 50 without warning. Likewise, if the passed price is zero, it will cause revert in the JBSingleTokenPaymentTerminalStore
1, 2, 3, because the value is used as denominator.
// example usage of the price information // line 661 JBSingleTokenPaymentTerminalStore 658 : PRBMath.mulDiv( 659 _amount, 660 10**_MAX_FIXED_POINT_FIDELITY, // Use _MAX_FIXED_POINT_FIDELITY to keep as much of the `_amount.value`'s fidelity as possible when converting. 661: prices.priceFor(_currency, _balanceCurrency, _MAX_FIXED_POINT_FIDELITY) 662 );
none
// example (uint80 roundID, int256 _price, , uint256 timestamp, uint80 answeredInRound) = feed.latestRoundData(); require(_price > 0, "ChainLink: price <= 0"); require(answeredInRound >= roundID, "ChainLink: Stale price"); require(timestamp > 0, "ChainLink: Round not complete");
#0 - mejango
2022-07-12T18:57:10Z
dup of #138
π Selected for report: zzzitron
4288.0611 USDC - $4,288.06
MED - the function of the protocol could be impacted
The proof of concept demonstrates to discard one of duplicated locked splits. In the beginning it launches a project with two identical locked splits. As the owner of the project, it updates splits to only one of the two splits. Since all of original splits are locked both of them should still in the split after the update, but only one of them exists in the updated splits.
It happens because the check of the locked split is not suitable for duplicated cases.
// JBSplitsStore::_set 203 // Check to see if all locked splits are included. 204 for (uint256 _i = 0; _i < _currentSplits.length; _i++) { 205 // If not locked, continue. 206 if (block.timestamp >= _currentSplits[_i].lockedUntil) continue; 207 208 // Keep a reference to whether or not the locked split being iterated on is included. 209 bool _includesLocked = false; 210 211 for (uint256 _j = 0; _j < _splits.length; _j++) { 212 // Check for sameness. 213 if ( 214 _splits[_j].percent == _currentSplits[_i].percent && 215 _splits[_j].beneficiary == _currentSplits[_i].beneficiary && 216 _splits[_j].allocator == _currentSplits[_i].allocator && 217 _splits[_j].projectId == _currentSplits[_i].projectId && 218 // Allow lock extention. 219 _splits[_j].lockedUntil >= _currentSplits[_i].lockedUntil 220 ) _includesLocked = true; 221 } 222 223 if (!_includesLocked) revert PREVIOUS_LOCKED_SPLITS_NOT_INCLUDED(); 224 }
None
Either prevent duplicates in the splits or track the matches while checking the locked splits.
1929.6275 USDC - $1,929.63
https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBFundingCycleStore.sol#L306-L312 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBFundingCycleStore.sol#L518-L522
MED - the function of the protocol could be impacted
By setting huge mustStartAtOrAfter
, the owner can set start time in the past. It might open up possibility to bypass the ballot waiting time depending on the ballot's implementation.
The proof of concept is almost the same as TestReconfigure::testReconfigureProject
. In the original test, the owner of the project is reconfiguring funding cycle, but it is not in effect immediately because ballot is set. Only after 3 days the newly set funding cycle will be the current one.
In the above proof of concept, only one parameter of the funding cycle is modified: mustStartAtOrAfter
is set to type(uint56).max
. As the result, the newly set funding cycle is considered as the current one without waiting for the ballot.
The cause of this is missing check on mustStartAtOrAfter
upon setting here. If the given _mustStartAtOrAfter
is huge, it will be passed eventually to the _initFor
, _packAndStoreIntrinsicPropertiesOf
. Then it will 'overflow' by shifting and set to the funding cycle, which essentially can be set to any value including the past. Also, it seems like the number will be also effected because the bigger digit will carry over.
// in JBFundingCycleStore::_packAndStoreIntrinsicPropertiesOf // where the `_start` is derived from `_mustStartAtOrAfter` ./JBFundingCycleStore.sol-518- // start in bits 144-199. ./JBFundingCycleStore.sol:519: packed |= _start << 144; ./JBFundingCycleStore.sol-520- ./JBFundingCycleStore.sol-521- // number in bits 200-255. ./JBFundingCycleStore.sol-522- packed |= _number << 200;
foundry
Add a check for the _mustStartAtOrAfter
:
// example check for _mustSTartAtOrAfter // in JBFundingCycleStore::configureFor if (_mustStartAtOrAfter > type(uint56).max) revert INVALID_START();
#0 - mejango
2022-07-12T20:15:20Z
interesting. needs test.
#1 - drgorillamd
2022-07-12T21:32:12Z
We've seen the poc, now assessing how to best mitigate (at what level)
#2 - jack-the-pug
2022-07-31T13:28:25Z
Good catch!
π 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
https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBERC20PaymentTerminal.sol#L99 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBERC20PaymentTerminal.sol#L87-L88
MED - hypothetical attack path with stated assumptions
Attacker can compromise assets by abusing missing return value check, when a project is using ERC20 token which does not revert on failure.
Some ERC20 tokens do not revert on failure for transfer, transferFrom and approve but return false. It is good to add checks on the return value if the contract is meant to be used for general ERC20 token.
// JBERC20PaymentTerminal.sol // The return values of the call transfer, transferFrom and approve are not checked 81 function _transferFrom( 82 address _from, 83 address payable _to, 84 uint256 _amount 85 ) internal override { 86 _from == address(this) 87 ? IERC20(token).transfer(_to, _amount) 88 : IERC20(token).transferFrom(_from, _to, _amount); 89 } //... 98 function _beforeTransferTo(address _to, uint256 _amount) internal override { 99 IERC20(token).approve(_to, _amount); 100 }
If the owner of project is using some ERC20 tokens which does not revert on failure but return false, users can abuse the lack of check. The user does not approve to send any funds to the terminal and the transferFrom
fails, but since the returned value is not checked the user will still get project's share.
It is also possible to add the existing terminal with such a token to another project and fake the balance and get the other project's fund out.
none
Add checks on the return values. But keep in mind some tokens do not return a bool (although it is the standard to return a bool). So to cover these cases use safeTransfer to check the return value only when there is one.
#0 - mejango
2022-07-12T18:37:32Z
sup of #281
π 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
237.8749 USDC - $237.87
Severity | Title |
---|---|
L00 | blocking recordPaymentFrom , recordRedemptionFor by setting mismatching information |
L01 | can set operator for projectId zero |
L02 | JBPayoutRedemptionPaymentTerminal cannot handle fee-on-transfer tokens |
recordPaymentFrom
, recordRedemptionFor
by setting mismatching informationJBSingleTokenPaymentTerminalStore::recordPaymentFrom
can be blocked by setting fundingCycle.useDataSourceForPay
to true and fundingCycle.dataSource
to zero address.JBSingleTokenPaymentTerminalStore::recordRedemptionFor
can be blocked by setting fundingCycle.useDataSourceForRedeem
to true and fundingCycle.dataSource
to zero address.It happens because the metadata for fundingCycle is not checked.
However, the project owner can achieve the same result with pausing redemption or setting malicious dataSource contract. Therefore, reporting this issue as Low severity.
When setting controller for a project, it does not check whether the projectId is zero. The projectId starts from one, so projectId 0 should be forbidden. Somebody in the isAllowedToSetFirstController
list can set controller of zero projectId and set other variables for non existing zeroth project using the controller.
// JBDirectory::setControllerOf // in the below check, add projectId 223 if (projects.count() < _projectId) revert INVALID_PROJECT_ID_IN_DIRECTORY();
// JBPayoutRedemptionPaymentTerminal::addToBalanceOf // Transfer tokens to this terminal from the msg sender. _transferFrom(msg.sender, payable(address(this)), _amount);
The above code assumes the transferred balance will be added to the balance without any loss, therefore failing to consider fee-on-transfer tokens. It will result in recorded balance and real balance mismatch. As the result, some people will get more money than they should get (basically the fee amount). Eventually there will be missing funds, and some people will fail to withdraw. When terminals for fee-on-transfer tokens are shared it leads to a problem. Since projects can add terminals which other projects are using, some malicious project owner can mass up balance of other projects when they use fee-on-transfer tokens.
It is reported as Low severity because it was unclear whether the platform is suppose to support these tokens.