Platform: Code4rena
Start Date: 18/10/2022
Pot Size: $50,000 USDC
Total HM: 13
Participants: 67
Period: 5 days
Judge: Picodes
Total Solo HM: 7
Id: 172
League: ETH
Rank: 6/67
Findings: 3
Award: $2,739.30
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: 0x1f8b
2675.4584 USDC - $2,675.46
The initialize
method of the JBTiered721Delegate
contract has as a flag that the _store
argument is different from address(0)
, however, it can be initialized by anyone with this value to allow the project to continue with its usual initialization, the attacker could have interfered and modified the corresponding values to carry out an attack.
Looking at the method below, we highlight in green the parts that need to be initialized to prevent a call to store=address(0)
from failing.
function initialize( uint256 _projectId, IJBDirectory _directory, string memory _name, string memory _symbol, IJBFundingCycleStore _fundingCycleStore, string memory _baseUri, IJBTokenUriResolver _tokenUriResolver, string memory _contractUri, JB721PricingParams memory _pricing, IJBTiered721DelegateStore _store, JBTiered721Flags memory _flags ) public override { // Make the original un-initializable. require(address(this) != codeOrigin); // Stop re-initialization. require(address(store) == address(0)); // Initialize the sub class. JB721Delegate._initialize(_projectId, _directory, _name, _symbol); fundingCycleStore = _fundingCycleStore; store = _store; pricingCurrency = _pricing.currency; pricingDecimals = _pricing.decimals; prices = _pricing.prices; // Store the base URI if provided. + if (bytes(_baseUri).length != 0) _store.recordSetBaseUri(_baseUri); // Set the contract URI if provided. + if (bytes(_contractUri).length != 0) _store.recordSetContractUri(_contractUri); // Set the token URI resolver if provided. + if (_tokenUriResolver != IJBTokenUriResolver(address(0))) _store.recordSetTokenUriResolver(_tokenUriResolver); // Record adding the provided tiers. + if (_pricing.tiers.length > 0) _store.recordAddTiers(_pricing.tiers); // Set the flags if needed. if ( + _flags.lockReservedTokenChanges || + _flags.lockVotingUnitChanges || + _flags.lockManualMintingChanges || + _flags.pausable ) _store.recordFlags(_flags); // Transfer ownership to the initializer. _transferOwnership(msg.sender); }
So if the attacker initializes the contract as follows:
_baseUri
= ""_contractUri
= ""_tokenUriResolver
= address(0)
_pricing.tiers
= []_flags
= all false
The contract will be initialized and transfered the ownership to msg.sender
.
After that, the owner can call didPay
with the the fake data provided in JBTiered721Delegate.sol:221 and increase creditsOf
of anyone JBTiered721Delegate.sol:587 without touching any store
call.
store
address is not empty.#0 - trust1995
2022-10-23T22:49:46Z
initialialize can only be called once, and it is done directly from the deployer, so I believe it to be invalid.
#1 - drgorillamd
2022-10-24T11:27:37Z
Duplicate #174
#2 - c4-judge
2022-11-05T12:49:19Z
Picodes marked the issue as not a duplicate
#3 - Picodes
2022-11-05T12:59:37Z
I believe the finding to be valid if:
_store == address(0)
and the parameters as above so it does not revert in the normal processdidPay
didPay
to manipulate creditsOf
initialize
to set _store
to non zero and at this point it is like if nothing happened although creditsOf
has been manipulated#4 - c4-judge
2022-11-05T13:00:05Z
Picodes marked the issue as satisfactory
#5 - trust1995
2022-11-05T13:07:52Z
Agree that the finding is valid, I missed that it is possible that _store is never called in the initialization. Because there are multiple unlikely conditionals, I do think it fits Med severity more than high.
#6 - drgorillamd
2022-11-05T13:13:11Z
Hmm, this would require a spoof directory too (to bypass the isTerminalOf check) -> I'd mitigate with a check msg.value==data.value in the abstract delegate contract, ie if someone wants to do this, actually paying the credit is needed
@mejango @xBA5ED summoned
Def nice finding, ggwp!
#7 - Picodes
2022-11-05T14:04:28Z
I do agree than Med is more appropriate as it falls within centralization risks as ultimately only the deployer could exploit this
#8 - c4-judge
2022-11-05T14:04:34Z
Picodes marked the issue as change severity
🌟 Selected for report: berndartmueller
Also found by: 0x1f8b, 0x4non, 0xNazgul, 0xSmartContract, Aymen0909, BClabs, Diana, Jeiwan, Lambda, LeoS, RaoulSchaffranek, RaymondFam, RedOneN, ReyAdmirado, Rolezn, SaharAP, Trust, V_B, __141345__, a12jmx, bharg4v, brgltd, carlitox477, ch0bu, chaduke, cloudjunky, cryptostellar5, cryptphi, csanuragjain, d3e4, delfin454000, erictee, fatherOfBlocks, hansfriese, ignacio, joestakey, karanctf, ladboy233, lukris02, mcwildy, minhtrng, peanuts, ret2basic, seyni, slowmoses, svskaushik, tnevler, yixxas
37.8829 USDC - $37.88
Some used packages are out of date, it is good practice to use the latest version of these packages:
"@openzeppelin/contracts": "^4.6.0"
Reference:
string.concat
instead of abi.encodePacked
Tthere is a discussion about removing abi.encodePacked
from future versions of Solidity (ethereum/solidity#11593), so using abi.encode
now will ensure compatibility in the future.
Affected source code:
Keep in mind that the version of solidity used, despite being greater than 0.8
, does not prevent integer overflows during casting, it only does so in mathematical operations.
It is necessary to safely convert between the different numeric types.
Recommendation:
Use a safeCast from Open Zeppelin.
function tierIdOfToken(uint256 _tokenId) public pure override returns (uint256) { // The tier ID is in the first 16 bits. return uint256(uint16(_tokenId)); }
Affected source code:
supportsInterface
The EIP-165
standard helps detect that a smart contract implements the expected logic, prevents human error when configuring smart contract bindings, so it is recommended to check that the received argument is a contract and supports the expected interface.
Reference:
Affected source code:
initialize
protectionsThe following contracts are updateable, and follow the update pattern, these contracts contain an initialize
method to set the contract once, but anyone can call this method, this will spend project fuel if someone tries to initialize it before the project.
It is recommended to check the sender when initializing the contract.
Affected source code:
#0 - drgorillamd
2022-10-27T20:23:57Z
4 and 5 are pretty generic, feels like it is not for our codebase (EIP165 is implemented in the contract quoted and the delagate is not updatable, initialize is to palliate the lack of constructor, as the contract is cloned)
#1 - Picodes
2022-11-04T10:43:55Z
1, 2, 3, 4 are valid.
For 2, the comment mentions abi.encode
which is not the same as string.concat
or abi.encodePacked
#2 - c4-judge
2022-11-04T10:43:59Z
Picodes marked the issue as grade-b
🌟 Selected for report: Jeiwan
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xSmartContract, Awesome, Aymen0909, Bnke0x0, CodingNameKiki, Diana, DimSon, JC, JrNet, LeoS, RaymondFam, ReyAdmirado, Saintcode_, Shinchan, __141345__, berndartmueller, bharg4v, brgltd, carlitox477, ch0bu, chaduke, cryptostellar5, emrekocak, gogo, lukris02, martin, mcwildy, sakman, trustindistrust, zishansami
25.9629 USDC - $25.96
It's cheaper to store the length of the array inside a local variable and iterate over it.
Affected source code:
Using compound assignment operators for state variables (like State += X
or State -= X
...) it's more expensive than using operator assignment (like State = State + X
or State = State - X
...).
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { uint private _a; function testShort() public { _a += 1; } } contract TesterB { uint private _a; function testLong() public { _a = _a + 1; } }
Gas saving executing: 13 per entry
TesterA.testShort: 43507 TesterB.testLong: 43494
Affected source code:
string.concat
instead of abi.encodePacked
abi.encodePacked
has a cost difference with respect to string.concat
depending on the types it uses. In the case of the reference shown below you can see that the type address
is affected, so I have decided to do a test on it.
Also there is a discussion about removing abi.encodePacked
from future versions of Solidity (ethereum/solidity#11593), so using abi.encode
now will ensure compatibility in the future.
Affected source code:
calldata
instead of memory
Some methods are declared as external
but the arguments are defined as memory
instead of as calldata
.
By marking the function as external
it is possible to use calldata
in the arguments shown below and save significant gas.
Recommended change:
- function mintReservesFor(JBTiered721MintReservesForTiersData[] memory _mintReservesForTiersData) + function mintReservesFor(JBTiered721MintReservesForTiersData[] calldata _mintReservesForTiersData) external override { ... }
Affected source code:
++i
costs less gas compared to i++
or i += 1
++i
costs less gas compared to i++
or i += 1
for unsigned integers, 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--
Keep in mind that this change can only be made when we are not interested in the value returned by the operation, since the result is different, you only have to apply it when you only want to increase a counter.
Affected source code:
Custom errors from Solidity 0.8.4
are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source Custom Errors in Solidity:
Starting from Solidity v0.8.4
, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { function testRevert(bool path) public view { require(path, "test error"); } } contract TesterB { error MyError(string msg); function testError(bool path) public view { if(path) revert MyError("test error"); } }
Gas saving executing: 9 per entry
TesterA.testRevert: 21611 TesterB.testError: 21602
Affected source code:
delete
optimizationUse delete
instead of set to default value (false
or 0
).
5 gas could be saved per entry in the following affected lines:
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.
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { function testInit() public view returns (uint) { uint a = 0; return a; } } contract TesterB { function testNoInit() public view returns (uint) { uint a; return a; } }
Gas saving executing: 8 per entry
TesterA.testInit: 21392 TesterB.testNoInit: 21384
Affected source code:
Remove natspec complaints in order to save gas. This is not the best way to avoid some warnings.
Affected source code:
unchecked
keywordWhen an underflow or overflow cannot occur, one might conserve gas by using the unchecked
keyword to prevent unnecessary arithmetic underflow/overflow tests.
Affected source code:
#0 - drgorillamd
2022-10-27T20:29:01Z
Remove natspec complaints in order to save gas.
?
#1 - Picodes
2022-11-04T10:50:04Z
1, 5, 6, 9 are out of scope or invalid
For example 6 there is no string in the code's require
#2 - Picodes
2022-11-04T10:52:41Z
Overall this does really look like a tool output without further triage
#3 - c4-judge
2022-11-04T10:52:49Z
Picodes marked the issue as grade-b
#4 - c4-judge
2022-11-04T10:52:58Z
Picodes marked the issue as grade-c
#5 - c4-judge
2022-11-04T10:53:12Z
Picodes marked the issue as grade-b