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: 33/105
Findings: 3
Award: $162.83
🌟 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
JBChainlinkV3PriceFeed.sol
we are using latestRoundData, but there is no check if the return value indicates stale data..Even though its only getting the price
variable, the whole latestRoundData
function gets returned and we cant just ignore it because the price
and latestRoundData
can be stale with out knowing it.
function currentPrice(uint256 _decimals) external view override returns (uint256) { // Get the latest round information. Only need the price is needed. (, int256 _price, , , ) = feed.latestRoundData();
This could lead to stale prices according to the Chainlink documentation: https://docs.chain.link/docs/historical-price-data/#historical-rounds https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round
Vim
At best use this
require( price > 0,"Chainlink answer reporting 0");
If want to make sure everything is correct get the roundID and timestamp to totally make sure there is no stale data.
require(answeredInRound >= roundID, "Stale price"); require(timestamp != 0,"Round not complete"); require(price > 0,"Chainlink answer reporting 0");
#0 - mejango
2022-07-12T18:47:31Z
dup #138
🌟 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
https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/c363abb67302314c2e061a01f76eb5e5dce2c935/contracts/JBTokenStore.sol#L188 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L562
an attacker can make their own project but with same symbol
and name
and make it look execaully like a real project that its using juicebox and with the fake project the attacker can steal users funds
function issueTokenFor(   uint256 _projectId,   string calldata _name,   string calldata _symbol  )   external   virtual   override   requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.ISSUE)   returns (IJBToken token)  {   // Issue the token in the store.   return tokenStore.issueFor(_projectId, _name, _symbol);  }
function issueFor(   uint256 _projectId,   string calldata _name,   string calldata _symbol  ) external override onlyController(_projectId) returns (IJBToken token) {   // There must be a name.   if (bytes(_name).length == 0) revert EMPTY_NAME();   // There must be a symbol.   if (bytes(_symbol).length == 0) revert EMPTY_SYMBOL();   // The project shouldn't already have a token.   if (tokenOf[_projectId] != IJBToken(address(0))) revert PROJECT_ALREADY_HAS_TOKEN();   // Deploy the token contract.   token = new JBToken(_name, _symbol);   // Store the token contract.   tokenOf[_projectId] = token;   // Store the project for the token.   projectOf[token] = _projectId;
issueFor
functionaave
that the attacker wants to phish
,the attacker supplies _name
and _symbol
as the same as aavevim
I would make sure on the front end side that _name
, _symbol
have to be orignal ( no same name or symbol can be used twice) or you can have the frontend make verfied projects that show up on the frontend to mitigate most of this risk.
#0 - mejango
2022-07-08T22:11:16Z
this is a good risk to note in the protocol docs, thank you.
#1 - jack-the-pug
2022-07-23T23:58:44Z
Good QA issue.
I'm afraid the suggested change would introduce a new DoS attack vector, though:
make sure on the front end side that _name, _symbol have to be orignal ( no same name or symbol can be used twice)
🌟 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.8282 USDC - $38.83
++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
JBController.sol:913: for (uint256 _i = 0; _i < _splits.length; _i++) { JBController.sol:1014: for (uint256 _i; _i < _fundAccessConstraints.length; _i++) { 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++) JBETHERC20SplitsPayer.sol:466: for (uint256 i = 0; i < _splits.length; i++) { JBFundingCycleStore.sol:724: for (uint256 i = 0; i < _discountMultiple; 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++) { JBSingleTokenPaymentTerminalStore.sol:862: for (uint256 _i = 0; _i < _terminals.length; _i++) JBSplitsStore.sol:204: for (uint256 _i = 0; _i < _currentSplits.length; _i++) { JBSplitsStore.sol:229: for (uint256 _i = 0; _i < _splits.length; _i++) { JBSplitsStore.sol:304: for (uint256 _i = 0; _i < _splitCount; _i++) {
sstore: https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/5ddb8136372ba06b5afe29156bff898335dd1fd1/contracts/JBProjects.sol#L40 mmstore:
abstract/JBPayoutRedemptionPaymentTerminal.sol:594: for (uint256 _i = 0; _i < _heldFeeLength; ) { abstract/JBPayoutRedemptionPaymentTerminal.sol:1008: for (uint256 _i = 0; _i < _splits.length; ) { abstract/JBPayoutRedemptionPaymentTerminal.sol:1396: for (uint256 _i = 0; _i < _heldFeesLength; ) { JBController.sol:913: for (uint256 _i = 0; _i < _splits.length; _i++) { JBController.sol:1014: for (uint256 _i; _i < _fundAccessConstraints.length; _i++) { JBDirectory.sol:25: using JBGlobalFundingCycleMetadataResolver for uint8; 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++) JBETHERC20SplitsPayer.sol:466: for (uint256 i = 0; i < _splits.length; i++) { JBFundingCycleStore.sol:724: for (uint256 i = 0; i < _discountMultiple; 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++) { JBSingleTokenPaymentTerminalStore.sol:862: for (uint256 _i = 0; _i < _terminals.length; _i++) JBSplitsStore.sol:165: for (uint256 _i = 0; _i < _groupedSplitsLength; ) { 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++) {
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: Checked or Unchecked Arithmetic. I suggest wrapping with an unchecked block: Same thing with second unchecked because total can't overflow amount cant overflow
abstract/JBPayoutRedemptionPaymentTerminal.sol:594: for (uint256 _i = 0; _i < _heldFeeLength; ) { abstract/JBPayoutRedemptionPaymentTerminal.sol:1008: for (uint256 _i = 0; _i < _splits.length; ) { abstract/JBPayoutRedemptionPaymentTerminal.sol:1396: for (uint256 _i = 0; _i < _heldFeesLength; ) { JBController.sol:913: for (uint256 _i = 0; _i < _splits.length; _i++) { JBController.sol:1014: for (uint256 _i; _i < _fundAccessConstraints.length; _i++) { JBDirectory.sol:25: using JBGlobalFundingCycleMetadataResolver for uint8; 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++) JBETHERC20SplitsPayer.sol:466: for (uint256 i = 0; i < _splits.length; i++) { JBFundingCycleStore.sol:724: for (uint256 i = 0; i < _discountMultiple; 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++) { JBSingleTokenPaymentTerminalStore.sol:862: for (uint256 _i = 0; _i < _terminals.length; _i++) JBSplitsStore.sol:165: for (uint256 _i = 0; _i < _groupedSplitsLength; ) { 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++) {
abstract/JBPayoutRedemptionPaymentTerminal.sol:594: for (uint256 _i = 0; _i < _heldFeeLength; ) { abstract/JBPayoutRedemptionPaymentTerminal.sol:1008: for (uint256 _i = 0; _i < _splits.length; ) { abstract/JBPayoutRedemptionPaymentTerminal.sol:1396: for (uint256 _i = 0; _i < _heldFeesLength; ) { JBController.sol:913: for (uint256 _i = 0; _i < _splits.length; _i++) { JBController.sol:1014: for (uint256 _i; _i < _fundAccessConstraints.length; _i++) { JBDirectory.sol:25: using JBGlobalFundingCycleMetadataResolver for uint8; 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++) JBETHERC20SplitsPayer.sol:466: for (uint256 i = 0; i < _splits.length; i++) { JBFundingCycleStore.sol:724: for (uint256 i = 0; i < _discountMultiple; 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++) { JBSingleTokenPaymentTerminalStore.sol:862: for (uint256 _i = 0; _i < _terminals.length; _i++) JBSplitsStore.sol:165: for (uint256 _i = 0; _i < _groupedSplitsLength; ) { 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++) {
0x0000000000000 to save gas
JBTokenStore.sol:123: if (_token != IJBToken(address(0))) totalSupply = totalSupply + _token.totalSupply(_projectId); JBTokenStore.sol:148: if (_token != IJBToken(address(0))) balance = balance + _token.balanceOf(_holder, _projectId); JBTokenStore.sol:200: if (tokenOf[_projectId] != IJBToken(address(0))) revert PROJECT_ALREADY_HAS_TOKEN(); JBTokenStore.sol:242: if (_token == IJBToken(address(0)) && requireClaimFor[_projectId]) JBTokenStore.sol:249: if (_token != IJBToken(address(0)) && _token.decimals() != 18) JBTokenStore.sol:259: if (_token != IJBToken(address(0))) projectOf[_token] = _projectId; JBTokenStore.sol:262: if (oldToken != IJBToken(address(0))) projectOf[oldToken] = 0; JBTokenStore.sol:265: if (_newOwner != address(0) && oldToken != IJBToken(address(0))) JBTokenStore.sol:294: _token != IJBToken(address(0)); JBTokenStore.sol:333: uint256 _claimedBalance = _token == IJBToken(address(0)) JBTokenStore.sol:400: if (_token == IJBToken(address(0))) revert TOKEN_NOT_FOUND(); JBTokenStore.sol:439: if (_recipient == address(0)) revert RECIPIENT_ZERO_ADDRESS(); JBTokenStore.sol:477: if (_token == IJBToken(address(0))) revert TOKEN_NOT_FOUND(); JBPrices.sol:69: if (_feed != IJBPriceFeed(address(0))) return _feed.currentPrice(_decimals); JBPrices.sol:75: if (_feed != IJBPriceFeed(address(0))) JBPrices.sol:115: if (feedFor[_currency][_base] != IJBPriceFeed(address(0))) revert PRICE_FEED_ALREADY_EXISTS(); JBProjects.sol:71: if (tokenUriResolver == IJBTokenUriResolver(address(0))) return ''; JBFundingCycleStore.sol:346: _data.ballot != IJBFundingCycleBallot(address(0)) || JBETHERC20SplitsPayer.sol:536: _split.beneficiary != address(0) ? JBETHERC20SplitsPayer.sol:480: if (_split.allocator != IJBSplitAllocator(address(0))) { JBController.sol:930: _split.allocator != IJBSplitAllocator(address(0)) JBController.sol:934: : _split.beneficiary != address(0) JBController.sol:943: if (_split.allocator != IJBSplitAllocator(address(0))) JBDirectory.sol:103: @return An array of terminal addresses. JBDirectory.sol:134: _primaryTerminalOf[_projectId][_token] != IJBPaymentTerminal(address(0)) && JBDirectory.sol:145: return IJBPaymentTerminal(address(0)); JBDirectory.sol:180: @param _owner The address that will own the contract. JBDirectory.sol:206: - or, an allowedlisted address is setting a controller for a project that doesn't already have a controller. JBDirectory.sol:219: (isAllowedToSetFirstController[msg.sender] && controllerOf[_projectId] == address(0))) JBDirectory.sol:230: msg.sender != address(controllerOf[_projectId]) && JBDirectory.sol:231: controllerOf[_projectId] != address(0)
becuase there is no check msg.value ==0
abstract/JBPayoutRedemptionPaymentTerminal.sol:622: function setFee(uint256 _fee) external virtual override onlyOwner { abstract/JBPayoutRedemptionPaymentTerminal.sol:644: function setFeeGauge(IJBFeeGauge _feeGauge) external virtual override onlyOwner { abstract/JBPayoutRedemptionPaymentTerminal.sol:661: function setFeelessAddress(address _address, bool _flag) external virtual override onlyOwner { JBDirectory.sol:336: onlyOwner JBETHERC20ProjectPayer.sol:203: ) external virtual override onlyOwner { JBETHERC20SplitsPayer.sol:209: ) external virtual override onlyOwner { JBPrices.sol:113: ) external override onlyOwner { JBProjects.sol:179: function setTokenUriResolver(IJBTokenUriResolver _newResolver) external override onlyOwner { JBToken.sol:110: ) external override onlyOwner { JBToken.sol:131: ) external override onlyOwner { JBToken.sol:211: onlyOwner
// 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.
JBTokenStore.sol:101: mapping(uint256 => bool) public override requireClaimFor;
JBChainlinkV3PriceFeed.sol JBERC20PaymentTerminal.sol JBETHERC20SplitsPayerDeployer.sol JBFundingCycleStore.sol JBProjects.sol JBSplitsStore.sol libraries enums JBController.sol JBETHERC20ProjectPayerDeployer.sol JBETHERC20SplitsPayer.sol JBOperatorStore.sol JBReconfigurationBufferBallot.sol JBToken.sol structs interfaces JBDirectory.sol JBETHERC20ProjectPayer.sol JBETHPaymentTerminal.sol JBPrices.sol JBSingleTokenPaymentTerminalStore.sol JBTokenStore.sol