Platform: Code4rena
Start Date: 18/05/2023
Pot Size: $24,500 USDC
Total HM: 3
Participants: 72
Period: 4 days
Judge: LSDan
Id: 237
League: ETH
Rank: 21/72
Findings: 1
Award: $235.20
🌟 Selected for report: 0
🚀 Solo Findings: 0
235.1974 USDC - $235.20
6/8 optimizations were benchmarked using the protocol's tests, i.e. using the following config: solc version 0.8.19, optimizer on, 200 runs and forge test --gas-report
Total: 9 instances over 8 issues Total gas saved: 364
The PRBMath library provides a mulDivFixedPoint() function where the dominator is always 10 ** 18. Because the dominator is always 10 ** 18 there this makes this function cheaper than the mulDiv() function where you pass in the dominator. Because the mulDiv() function uses 10 ** 18 in payParams() it can be replaced with mulDivFixedPoint() to save gas
Gas Savings for payParams() obtained via protocol's tests: Avg 150 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 2128 | 8321 | 10024 | 12529 | 10 |
After | 2075 | 8271 | 9981 | 12476 | 10 |
There is 1 instance of this issue:
File: contracts\JBXBuybackDelegate.sol 150: uint256 _tokenCount = PRBMath.mulDiv(_data.amount.value, _data.weight, 10 ** 18);
Should be replaced with:
File: contracts\JBXBuybackDelegate.sol 150: uint256 _tokenCount = PRBMath.mulDivFixedPoint(_data.amount.value, _data.weight);
When you are minting in the payParams() function it creates and then returns an empty JBPayDelegateAllocation array. However, even if the length of the array you are creating is 0, it still costs gas. Using the empty return array delegateAllocations will cost less gas than creating a new one and then returning it
Gas Savings for payParams() obtained via protocol's tests: Avg 67 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 2128 | 8321 | 10024 | 12529 | 10 |
After | 2019 | 8254 | 9981 | 12476 | 10 |
There is 1 instance of this issue:
File: contracts\JBXBuybackDelegate.sol 170: return (_data.weight, _data.memo, new JBPayDelegateAllocation[](0));
Should be replaced with:
File: contracts\JBXBuybackDelegate.sol 170: return (_data.weight, _data.memo, delegateAllocations);
An empty string literal is cheaper than creating new bytes(0) and paying more for the initialization.
Gas Savings for didPay() mint obtained via protocol's tests: Avg 74 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 51675 | 161126 | 148266 | 242106 | 7 |
After | 51675 | 161052 | 148266 | 242106 | 7 |
There is 1 instance of this issue:
File: contracts\JBXBuybackDelegate.sol 348: jbxTerminal.addToBalanceOf{value: _data.amount.value}( 349: _data.projectId, _data.amount.value, JBTokens.ETH, "", new bytes(0) 350: );
Should be replaced with:
File: contracts\JBXBuybackDelegate.sol 348: jbxTerminal.addToBalanceOf{value: _data.amount.value}( 349: _data.projectId, _data.amount.value, JBTokens.ETH, "", "" 350: );
Setting amountReceived and amountToSend using ternary operators will cost more than using if/else
There is 1 instance of this issue: https://github.com/code-423n4/2023-05-juicebox/blob/main/juice-buyback/contracts/JBXBuybackDelegate.sol#L224-L225
Gas Savings for uniswapV3SwapCallback() obtained via protocol's tests: Avg 7 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 867 | 20125 | 28794 | 30714 | 6 |
After | 859 | 20118 | 28788 | 30708 | 6 |
File: contracts\JBXBuybackDelegate.sol 224: uint256 _amountReceived = uint256(-(_projectTokenIsZero ? amount0Delta : amount1Delta)); 225: uint256 _amountToSend = uint256(_projectTokenIsZero ? amount1Delta : amount0Delta);
Should be replaced with:
File: contracts\JBXBuybackDelegate.sol uint256 _amountReceived; uint256 _amountToSend; if(_projectTokenIsZero) { _amountReceived = uint256(-(amount0Delta)); _amountToSend = uint256(amount1Delta); } else { _amountReceived = uint256(-(amount1Delta)); _amountToSend = uint256(amount0Delta); }
In the swap function, the reserverToken is set before the transfer and is only used after the transfer. It would be better to put it after the transfer function because the transfer can easily fail and you would have to pay for operations that you used to set the reservedToken
Gas Savings for didPay() in the revert case when the transfer fails: Avg 53 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 2948 | 109288 | 112680 | 205775 | 6 |
After | 2882 | 109235 | 112597 | 205775 | 6 |
There is 1 instance of this issue:
File: contracts\JBXBuybackDelegate.sol 283: uint256 _reservedToken = _amountReceived - _nonReservedToken; 284: 285: // Send the non-reserved token to the beneficiary (if any / reserved rate is not max) 286: if (_nonReservedToken != 0) projectToken.transfer(_data.beneficiary, _nonReservedToken);
Should be replaced with:
File: contracts\JBXBuybackDelegate.sol if (_nonReservedToken != 0) projectToken.transfer(_data.beneficiary, _nonReservedToken); uint256 _reservedToken = _amountReceived - _nonReservedToken;
The swap and mint functions both access the data.projectId multiple times. Calldata loading is cheap but if you have a struct here it would be better to cache the projectId and then use it
Gas Savings for didPay() swap btained via protocol's tests: Avg 11 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 51675 | 161126 | 148266 | 242106 | 7 |
After | 51655 | 161115 | 148246 | 242086 | 7 |
Gas Savings for didPay() mint obtained via protocol's tests: Avg 2 gas
MIN | AVG | MED | MAX | # CALLS | |
---|---|---|---|---|---|
Before | 51675 | 161126 | 148266 | 242106 | 7 |
After | 51675 | 161124 | 148266 | 242106 | 7 |
There is 2 instance of this issue:
https://github.com/code-423n4/2023-05-juicebox/blob/main/juice-buyback/contracts/JBXBuybackDelegate.sol#L258 https://github.com/code-423n4/2023-05-juicebox/blob/main/juice-buyback/contracts/JBXBuybackDelegate.sol#L334
File: contracts\JBXBuybackDelegate.sol ///@audit The projectId should be cached like this; uint256 projectId = _data.projectId; IJBController controller = IJBController(jbxTerminal.directory().controllerOf(projectId));
abi.encodePacked is cheaper than abi.encode() because packed encoding uses less data and creates a smaller output
Read more here
There is 1 instance of this issue:
File: contracts\JBXBuybackDelegate.sol 268: data: abi.encode(_minimumReceivedFromSwap)
Should be replaced with:
File: contracts\JBXBuybackDelegate.sol abi.encodePacked(_minimumReceivedFromSwap)
You can save gas by ordering contracts most called functions via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save gas if you order the functions correctly
See this link to see how it works
#0 - c4-pre-sort
2023-05-25T13:59:26Z
dmvt marked the issue as high quality report
#1 - c4-judge
2023-06-02T12:11:44Z
dmvt marked the issue as grade-a
#2 - drgorillamd
2023-06-07T14:35:51Z
Thanks for the report! Some comments:
reservedToken should be set after the transfer is made so you can save gas if the transfer reverts
this is the transfer of the project token received from the swap, this one should never revert
abi.encodePacked() is cheaper than abi.encode()
not here, compared and exact same cost (with 200 runs) - this might be true with multiple data tho (packed vs unpacked then), with the collision risk it introduces
Optimize names to save gas
Every function of the datasource/delegate will be called once per tx (ie A-B-C has same cost if reordering to C-B-A here