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: 23/72
Findings: 2
Award: $44.17
š Selected for report: 0
š Solo Findings: 0
š Selected for report: ABA
Also found by: 0x4non, 0xHati, 0xMosh, 0xSmartContract, 0xWaitress, 0xhacksmithh, 0xnev, 0xprinc, Arabadzhiev, BLACK-PANDA-REACH, Deekshith99, Dimagu, KKat7531, Kose, LosPollosHermanos, MohammedRizwan, QiuhaoLi, RaymondFam, Rickard, Rolezn, SAAJ, Sathish9098, Shubham, SmartGooofy, Tripathi, Udsen, V1235816, adriro, arpit, ayden, bigtone, codeVolcan, d3e4, dwward3n, fatherOfBlocks, favelanky, jovemjeune, kutugu, lfzkoala, lukris02, matrix_0wl, minhquanym, ni8mare, parsely, pxng0lin, radev_sw, ravikiranweb3, rbserver, sces60107, souilos, tnevler, turvy_fuzz, yellowBirdy
16.1907 USDC - $16.19
JBXBuybackDelegate.payParams
in line 153Currently, uint256 _slippage
and _quote
variable is not sanitized, and an arbitrary value could be assigned to it through _data.metadata
.
This could lead to the follwoing unexpected behavior:
_slippage
to be greater than SLIPPAGE_DENOMINATOR
, which will lead to _slippage / SLIPPAGE_DENOMINATOR > 1
, and the equation:
_quote - (_quote * _slippage / SLIPPAGE_DENOMINATOR)
will cause an arithmetic underflow_slipage
can be arbitrarly set to 0, which might lead to miscalculations of either the protocol should mint or swap tokens_quote
for the token pair inside JBXBuybackDelegate.payParams
instead of passing it through jbEthPaymentTerminal.pay()
function through the _metadata
. Currently, arbitrary value can be passed for _quote
which might lead to to miscalculations of either the protocol should mint or swap tokens.As anyone can call this function, a malicious user can set the value of the 2 mutex _mintedAmount and _reservedRate
to 0, making every call much more expensive (cold variable access)
(uint256 \_minimumAmountReceived)
in line 221 is presented as a tuple, but it is a single value. The parenthesis are not necessary.
0.8.16/17/18
UniswapV3ForgeQuoter.sol
referenced in package.json line 13 is written in solidity compiler version 0.8.19
, which causes a compilation in solidity versions 0.8.16/17/18
to fail.
import "@jbx-protocol/juice-contracts-v3/contracts/interfaces/IJBFundingCycleBallot.sol";
is not needed, line 8import "@openzeppelin/contracts/access/Ownable.sol";
is not needed, line 15JBXBuybackDelegate
contract to inherit Ownable
contract, line 39SLIPPAGE_DENOMINATOR = 10000
should be changed to SLIPPAGE_DENOMINATOR = 10_000
for improved readability.
weigh
should be changed to weight
prefered
should be changed to preferred
Retrieve the number of token created
should be changed to Retrieve the number of tokens created
If swap failed, mint instead, with the original weight + add to balance the token in
should be changed to If swap failed, mint instead with the original weight + add to balance the token in
(where token transfer should happens)
should be changed to (where token transfer should happen)
Slippage controle is achieved here
should be changed to Slippage control is achieved here
Swap the terminal token to receive the project toke_beforeTransferTon
incomprehensible commentThis delegate first receive the whole amount of project token
should be changed to This delegate first receives the whole amount of project tokens
then burn the rest of this delegate balance (ie the amount of reserved token)
should be changed to then burn the rest of this delegate balance (ie the amount of reserved tokens)
If there are reserved token...
should be changed to If there are reserved tokens...
Burn all the reserved token...
should be changed to Burn all the reserved tokens...
Mint the reserved token with...
should be changed to Mint the reserved tokens with...
Burn the non-reserve token...
should be changed to Burn the non-reserve tokens...
the amount of token out to mint
should be changed to the amount of tokens out to mint
amountOut
to amountJBX
projectToken
use JBXToken
The uniswap pool corresponding to the project token - other token market
use The uniswap pool corresponding to the JBX token - wETH market
jbxTerminal
use jbEthPaymentTerminal
#0 - c4-judge
2023-06-02T11:03:19Z
dmvt marked the issue as grade-b
27.9811 USDC - $27.98
Instead of calculating TickMath.MAX_SQRT_RATIO - 1
and TickMath.MIN_SQRT_RATIO + 1
in line 267 every time swap is conducted, we can define this values as a constant variables.
Ex. uint160 private constant MAX_SQRT_RATIO_MINUS_ONE = 1461446703485210103287273052203988822378723970341;
& uint160 private constant MIN_SQRT_RATIO_PLUS_ONE = 4295128740;
, and we can get rid of import "@uniswap/v3-core/contracts/libraries/TickMath.sol";
library.
Gas Savings for JBXBuybackDelegate.didPay
, obtained via protocol's tests: Avg 838 gas
Med | Max | Avg | # calls | |
---|---|---|---|---|
Before | 148266 | 242106 | 161891 | 7 |
After | 148184 | 242024 | 161053 | 7 |
File: src/EthRouter.sol 22: import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; ā 258: function _swap(JBDidPayData calldata _data, uint256 _minimumReceivedFromSwap, uint256 _reservedRate) 259: internal 260: returns (uint256 _amountReceived) 261: { 262: // Pass the token and min amount to receive as extra data 263: try pool.swap({ 264: recipient: address(this), 265: zeroForOne: !_projectTokenIsZero, 266: amountSpecified: int256(_data.amount.value), 267: sqrtPriceLimitX96: _projectTokenIsZero ? TickMath.MAX_SQRT_RATIO - 1 : TickMath.MIN_SQRT_RATIO + 1, 268: data: abi.encode(_minimumReceivedFromSwap) 269: }) returns (int256 amount0, int256 amount1) { 270: // Swap succeded, take note of the amount of projectToken received (negative as it is an exact input) 271: _amountReceived = uint256(-(_projectTokenIsZero ? amount0 : amount1)); 272: } catch { 273: // implies _amountReceived = 0 -> will later mint when back in didPay 274: return _amountReceived; 275: } 276: 277: // The amount to send to the beneficiary 278: uint256 _nonReservedToken = PRBMath.mulDiv( 279: _amountReceived, JBConstants.MAX_RESERVED_RATE - _reservedRate, JBConstants.MAX_RESERVED_RATE 280: ); 281: 282: // The amount to add to the reserved token 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); 287: 288: // If there are reserved token, add them to the reserve 289: if (_reservedToken != 0) { 290: IJBController controller = IJBController(jbxTerminal.directory().controllerOf(_data.projectId)); 291: 292: // 1) Burn all the reserved token, which are in this address -> result: 0 here, 0 in reserve 293: controller.burnTokensOf({ 294: _holder: address(this), 295: _projectId: _data.projectId, 296: _tokenCount: _reservedToken, 297: _memo: "", 298: _preferClaimedTokens: true 299: }); 300: 301: // 2) Mint the reserved token with this address as beneficiary -> result: _amountReceived-reserved here, reservedToken in reserve 302: controller.mintTokensOf({ 303: _projectId: _data.projectId, 304: _tokenCount: _amountReceived, 305: _beneficiary: address(this), 306: _memo: _data.memo, 307: _preferClaimedTokens: false, 308: _useReservedRate: true 309: }); 310: 311: // 3) Burn the non-reserve token which are now left in this address (can be 0) -> result: 0 here, reservedToken in reserve 312: uint256 _nonReservedTokenInContract = _amountReceived - _reservedToken; 313: 314: if (_nonReservedTokenInContract != 0) { 315: controller.burnTokensOf({ 316: _holder: address(this), 317: _projectId: _data.projectId, 318: _tokenCount: _nonReservedTokenInContract, 319: _memo: "", 320: _preferClaimedTokens: false 321: }); 322: } 323: } 324: 325: emit JBXBuybackDelegate_Swap(_data.projectId, _data.amount.value, _amountReceived); 326: }
ā
@@ -19,7 +19,6 @@ import "@paulrberg/contracts/math/PRBMath.sol"; ā import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; -import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; ā import "./interfaces/external/IWETH9.sol"; ā @@ -67,6 +66,16 @@ contract JBXBuybackDelegate is IJBFundingCycleDataSource, IJBPayDelegate, IUnisw */ uint256 private constant SLIPPAGE_DENOMINATOR = 10000; ā + /** + * @notice Uniswap's TickMath.MAX_SQRT_RATIO - 1 + */ + uint160 private constant MAX_SQRT_RATIO_MINUS_ONE = 1461446703485210103287273052203988822378723970341; + + /** + * @notice Uniswap's TickMath.MIN_SQRT_RATIO + 1 + */ + uint160 private constant MIN_SQRT_RATIO_PLUS_ONE = 4295128740; + //*********************************************************************// // --------------------- public constant properties ------------------ // //*********************************************************************// @@ -264,7 +273,7 @@ contract JBXBuybackDelegate is IJBFundingCycleDataSource, IJBPayDelegate, IUnisw recipient: address(this), zeroForOne: !_projectTokenIsZero, amountSpecified: int256(_data.amount.value), - sqrtPriceLimitX96: _projectTokenIsZero ? TickMath.MAX_SQRT_RATIO - 1 : TickMath.MIN_SQRT_RATIO + 1, + sqrtPriceLimitX96: _projectTokenIsZero ? MAX_SQRT_RATIO_MINUS_ONE : MIN_SQRT_RATIO_PLUS_ONE, data: abi.encode(_minimumReceivedFromSwap) }) returns (int256 amount0, int256 amount1) { // Swap succeded, take note of the amount of projectToken received (negative as it is an exact input)
We checked several lines of code where arithmetic is done that we proofed to be more gas efficient when using unchecked.
if (_slippage > SLIPPAGE_DENOMINATOR) { revert JuiceBuyback_InvalidSlippage(); } uint256 qs = _quote * _slippage; unchecked { // If the amount swapped is bigger than the lowest received when minting, use the swap pathway if (_tokenCount < _quote - (qs / SLIPPAGE_DENOMINATOR)) { // Pass the quote and reserve rate via a mutex mintedAmount = _tokenCount; reservedRate = _data.reservedRate; // Return this delegate as the one to use, and do not mint from the terminal delegateAllocations = new JBPayDelegateAllocation[](1); delegateAllocations[0] = JBPayDelegateAllocation({ delegate: IJBPayDelegate(this), amount: _data.amount.value }); return (0, _data.memo, delegateAllocations); } }
Gas Savings for JBXBuybackDelegate.payParams
, obtained via protocol's tests: Avg 84 gas
Med | Max | Avg | # calls | |
---|---|---|---|---|
Before | 11040 | 13800 | 9774 | 13 |
After | 10968 | 13709 | 9690 | 13 |
if (_slippage > SLIPPAGE_DENOMINATOR) { revert JuiceBuyback_InvalidSlippage(); } uint256 qs = _quote * _slippage; uint256 _minimumReceivedFromSwap; unchecked { _minimumReceivedFromSwap = _quote - (qs / SLIPPAGE_DENOMINATOR); }
Gas Savings for JBXBuybackDelegate.didPay
, obtained via protocol's tests: Avg 78 gas
Med | Max | Avg | # calls | |
---|---|---|---|---|
Before | 140264 | 246524 | 144896 | 10 |
After | 140187 | 246448 | 144818 | 7 |
if (_reservedRate > JBConstants.MAX_RESERVED_RATE) revert JuiceBuyback_InvalidReserveRate(); uint256 _nonReservedRate; unchecked { _nonReservedRate = JBConstants.MAX_RESERVED_RATE - _reservedRate; } // The amount to send to the beneficiary uint256 _nonReservedToken = PRBMath.mulDiv( _amountReceived, _nonReservedRate, JBConstants.MAX_RESERVED_RATE );
Gas Savings for JBXBuybackDelegate.didPay
, obtained via protocol's tests: Avg 15 gas
Med | Max | Avg | # calls | |
---|---|---|---|---|
Before | 140264 | 246524 | 144896 | 10 |
After | 140242 | 246503 | 144881 | 10 |
#0 - c4-judge
2023-06-02T10:53:39Z
dmvt marked the issue as grade-b