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: 1/72
Findings: 2
Award: $3,358.26
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: ABA
Also found by: RaymondFam, ktg
2914.7981 USDC - $2,914.80
For a funding cycle that is set to use the data source for redeem and the data source is JBXBuybackDelegate
, any and all redeemed tokens is 0 because of the returned 0 values from the empty redeemParams
implementation. This means the beneficiary would wrongly receive 0 tokens instead of an intended amount.
While JBXBuybackDelegate
was not designed to be used for redeem also, per protocol logic, this function should of been a pass-through (if no redeem such functionality was desired) because a redeem logic is by default used if not overwrite by redeemParams
, as per the documentation (further detailed below).
Impact is further increased as the function is not marked as virtual
, as such, no inheriting class can modify the faulty implementation if any such extension is desired.
Project logic dictates that a contract can become a treasury data source by adhering to IJBFundingCycleDataSource
, such as JBXBuybackDelegate
is and
that there are two functions that must be implemented, payParams(...)
and redeemParams(...)
.
Either one can be left empty if the intent is to only extend the treasury's pay functionality or redeem functionality.
But by left empty, it is specifically required to pass the input variables, not a 0 default value
https://docs.juicebox.money/dev/build/treasury-extensions/data-source/
Return the JBRedeemParamsData.reclaimAmount value if no altered functionality is desired. Return the JBRedeemParamsData.memo value if no altered functionality is desired. Return the zero address if no additional functionality is desired.
What should of been done:
// This is unused but needs to be included to fulfill IJBFundingCycleDataSource. function redeemParams(JBRedeemParamsData calldata _data) external pure override returns ( uint256 reclaimAmount, string memory memo, IJBRedemptionDelegate delegate ) { // Return the default values. return (_data.reclaimAmount.value, _data.memo, IJBRedemptionDelegate(address(0))); }
What is implemented:
function redeemParams(JBRedeemParamsData calldata _data) external override returns (uint256 reclaimAmount, string memory memo, JBRedemptionDelegateAllocation[] memory delegateAllocations) {}
While an overwritten memo
with a empty default string is not such a large issue, and incidentally delegateAllocations
is actually returned correctly as a zero address,
the issue lies with the reclaimAmount
amount which not is returned as 0.
Per documentation:
reclaimAmount
is the amount of tokens in the treasury that the terminal should send out to the redemption beneficiary as a result of burning the amount of project tokens tokens
and in case of redemptions, by default, the value is
a reclaim amount determined by the standard protocol bonding curve based on the redemption rate the project has configured into its current funding cycle which is provided to the data source function in JBRedeemParamsData.reclaimAmount
but in this case, it will be overwritten with 0 thus the redemption beneficiary will be deprived of funds.
redeemParams
is also lacking the virtual
keyword, as such, no inheriting class can modify the faulty implementation if any such extension is desired.
Manual analysis
Change the implementation of the function to respect protocol logic and add the virtual
keyword, example:
// This is unused but needs to be included to fulfill IJBFundingCycleDataSource. function redeemParams(JBRedeemParamsData calldata _data) external pure override virtual returns ( uint256 reclaimAmount, string memory memo, IJBRedemptionDelegate delegate ) { // Return the default values. return (_data.reclaimAmount.value, _data.memo, IJBRedemptionDelegate(address(0))); }
Any user/developer that wishes to extend the tool cannot or, at worst, uses a faulty implementation by mistake. Historically, medium severity was awarded to various issues which rely on some user error (in this case using a faulty implementation because docs indicate that all non-redeemable data sources are either pass-through, reverts or custom implementation), are easy to check/fix and present material risk. I respect this line of thought and for the sake of consistency I believe this submission should be judged similarly.
Other
#0 - c4-pre-sort
2023-05-24T17:49:31Z
dmvt marked the issue as high quality report
#1 - c4-pre-sort
2023-05-25T12:53:17Z
dmvt marked the issue as primary issue
#2 - drgorillamd
2023-05-27T19:11:53Z
The funding cycle has two delegate-related parameters, useDelegateForPay
and useDelegateForRedemption
. This is a pay delegate, to have a situation where the redemption becomes faulty would require the project owner to proactively submit a reconfiguration, to activate the use of the delegate on redemption. This is an user bug, not a protocol one
Should be documented to avoid confusion
#3 - c4-sponsor
2023-06-01T06:42:39Z
drgorillamd marked the issue as disagree with severity
#4 - dmvt
2023-06-02T15:10:17Z
I respect that this is user error, but I also think it's a very easy error to prevent. Medium stands.
#5 - c4-judge
2023-06-02T15:10:20Z
dmvt marked the issue as selected for report
🌟 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
443.4594 USDC - $443.46
During the audit, 1 low, 2 refactoring and 5 non-critical issues were found.
Total: 1 instance over 1 issues
# | Issue | Instances |
---|---|---|
[L-01] | JBXBuybackDelegate deployer should decide if held fees should be refunded | 1 |
Total: 2 instances over 2 issues
# | Issue | Instances |
---|---|---|
[R-01] | Better and documentation naming for _projectTokenIsZero | 1 |
[R-02] | Use named function calls | 1 |
Total: 13 instances over 5 issues
# | Issue | Instances |
---|---|---|
[NC-01] | Use JBTokenAmount::decimals instead of hardcoding it in payParams | 1 |
[NC-02] | Memo is not passed in all cases | 3 |
[NC-03] | Missing, misleading, incorrect, or incomplete comments | 7 |
[NC-04] | Events missing key information | 1 |
[NC-05] | Empty methods should be thoroughly documented | 1 |
JBXBuybackDelegate
deployer should decide if held fees should be refundedWhen minting of tokens is done, via JBXBuybackDelegate::_mint
, ETH is sent back to the terminal via a call to addToBalanceOf
.
There are 2 versions of addToBalanceOf
. One that accepts a flag _shouldRefundHeldFees
indicating if held fees should be refunded based on the amount being added.
and other, with one less argument, that is used by our protocol that sets it to false by default.
This option should not be hardcoded as it is, a contract deployer should determine if he opts in or not.
Add a constructor parameter that is then used to with the addToBalanceOf
when _mint
is called.
_projectTokenIsZero
A variable named _projectTokenIsZero
is used to indicate if the project token address is less then the address terminal token by sort order.
It describes it simply as:
/** * @notice Address project token < address terminal token ? */ bool private immutable _projectTokenIsZero;
then it is used to identify which swap return amount is to be used.
_amountReceived = uint256(-(_projectTokenIsZero ? amount0 : amount1));
This is needed because Uniswap IUniswapV3Pool::pool
returns the token amounts corresponding to the token sorted addresses order.
This is implicit because pools are created with the tokens as such (IUniswapV3Factory#PoolCreated)
The naming and description can be changed to better describe the need for this flag variable.
Add a more descriptive name and documentation. Example:
/** * @notice sores if: address project token < address terminal token * @dev used to identify which pair token (0 or 1) is the project token in a swap result */ bool private immutable _projectTokenIsPairToken0;
Code base has an extensive use of named function calls, but it somehow missed one instance where this would be appropriate.
jbxTerminal.addToBalanceOf{value: _data.amount.value}( _data.projectId, _data.amount.value, JBTokens.ETH, "", new bytes(0) );
Use named function calls on function call, as such:
jbxTerminal.addToBalanceOf{value: _data.amount.value}({ _projectId: _data.projectId, _amount: _data.amount.value, _token: JBTokens.ETH, _memo: "", _metadata: new bytes(0) });
JBTokenAmount::decimals
instead of hardcoding it in payParams
In payParams
the number of tokens to mint is determined by multiplying value with weight and dividing by 1e18.
// Find the total number of tokens to mint, as a fixed point number with 18 decimals uint256 _tokenCount = PRBMath.mulDiv(_data.amount.value, _data.weight, 10 ** 18);
As the token to be use by the project has 18 decimals this is not an issue for now. Any other project token to be added will require this modified in order to keep the premise that the total number of tokens to mint is a fixed point number with 18 decimals.
Out of the formula, _data.weight
has 18 decimals
// The weight according to which new token supply is to be minted, as a fixed point number with 18 decimals. uint256 _weight;
so the the 1e18 must be the decimal count of the JBTokenAmount
in order to be fully safe.
Use the decimals propriety of JBTokenAmount
instead of a hardcoded 1e18:
uint256 _tokenCount = PRBMath.mulDiv(_data.amount.value, _data.weight, 10 ** _data.amount.decimals);
All controller operations of mintTokensOf
and burnTokensOf
or terminal operation of addToBalanceOf
have a _memo
parameter.
The information in the memo will be passed to an event. The event is sent regardless so adding the memo will make off-chain analytics clearer.
https://github.com/code-423n4/2023-05-juicebox/blob/main/juice-buyback/contracts/JBXBuybackDelegate.sol#L297 https://github.com/code-423n4/2023-05-juicebox/blob/main/juice-buyback/contracts/JBXBuybackDelegate.sol#L319 https://github.com/code-423n4/2023-05-juicebox/blob/main/juice-buyback/contracts/JBXBuybackDelegate.sol#L349
Pass the memo information to the mentioned calls
There are cases where the comments are either missing, incomplete or incorrect throughout the codebase.
_swap
:
result:
are ambiguous _amountReceived-reserved here, reservedToken in reserve
, consider making them more clear, example: (_amountReceived - reserved) here, (reserved (token)) in reserve
(code)_mint
: at line 329 there is a double in in, remove one in (code)uniswapV3SwapCallback
:
Resolve the mentioned issues.
Some events are missing key information when emitted.
_mint
the JBXBuybackDelegate_Mint
event should contain the minted amount and beneficiary to whom it was minted (code)Add the missing information.
The function redeemParams
variable has no code implementation.
It is both lacking any NatSpec and any internal comments to indicate what is its purpose.
Properly document the method, why it is needed and why it is empty.
#0 - c4-pre-sort
2023-05-25T14:00:41Z
dmvt marked the issue as high quality report
#1 - c4-sponsor
2023-06-01T06:46:32Z
drgorillamd marked the issue as sponsor confirmed
#2 - c4-judge
2023-06-02T12:12:07Z
dmvt marked the issue as grade-a
#3 - c4-judge
2023-06-02T16:05:46Z
dmvt marked the issue as selected for report