Back to Blog
Juicebox Protocol: How to Prepare for an Audit of a Deployed Project
AuditDeFiSmart ContractUniswap

Juicebox Protocol: How to Prepare for an Audit of a Deployed Project

7 min
Most of the time, during public bug bounty contests, the project has not only been deployed already but also audited once or more times.
Therefore, you have to get used to analyzing the project and creating a summary from their website and documentation.
And the good thing is that in the meanwhile, you are reading about it, researching their existing codebase, and placing things together in order for it to make sense.
So, here's how I have prepared my summary for the Juicebox Buyback Delegate audit.

Overview

The Juicebox protocol is a framework for funding and operating projects openly on Ethereum. It is versatile and allows you to build anything from an NFT project to a boutique crypto law firm — meaning, almost any idea you want to implement.
  • You can launch a DAO with governance
  • All-in-one crowdfunding with powerful treasury management and redemptions
  • And as mentioned above, you can build and launch your NFT project
Juicebox NFT project
Juicebox governance
Juicebox governance minimal
Juicebox is a governance-minimal protocol. There are only a few levers that can be tuned, none of which impose changes for users without their consent.
And of course, the governance smart contract is responsible to adjust these levers.
Their governance token is the JBX token.

Architecture

The protocol is made up of 7 core contracts and 3 surface contracts.

Core contracts

Some of the most relevant core contracts are the following:

JBTokenStore

Manages token minting and burning for all projects.
Step-by-step explanation of mintFor:
1function mintFor(
2 address _holder,
3 uint256 _projectId,
4 uint256 _amount,
5 bool _preferClaimedTokens
6) external override onlyController(_projectId) { ... }
Step-by-step explanation of burnFrom:
1function burnFrom(
2 address _holder,
3 uint256 _projectId,
4 uint256 _amount,
5 bool _preferClaimedTokens
6) external override onlyController(_projectId) { ... }
Step-by-step explanation of issueFor:
1function issueFor(
2 uint256 _projectId,
3 string calldata _name,
4 string calldata _symbol
5)
6 external
7 override
8 requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.ISSUE)
9 returns (IJBToken token)
10{ ... }

JBSplitsStore

Stores information about how arbitrary distributions should be split. The information is represented as a JBSplit data structure.
  • A project can store splits for an arbitrary number of groups, such as for payout distributions or for reserved token distributions.
  • A split can specify an address, a Juicebox project, a contract that adheres to the IJBSplitAllocator interface, or the address that calls the transaction to distribute payouts or reserved tokens as its recipient.
  • By default, splits can be changed at any time for any funding cycle configuration. A project's owner can also independently lock a split to a funding cycle configuration for a customizable duration.
Step-by-step explanation of set:
1function set(
2 uint256 _projectId,
3 uint256 _domain,
4 JBGroupedSplits[] calldata _groupedSplits
5)
6 external
7 override
8 requirePermissionAllowingOverride(
9 projects.ownerOf(_projectId),
10 _projectId,
11 JBOperations.SET_SPLITS,
12 address(directory.controllerOf(_projectId)) == msg.sender
13 ) { ... }
And this is the JBSplit struct:
1/**
2 @member preferClaimed A flag that only has effect if a projectId is also
3 specified, and the project has a token contract attached. If so, this flag
4 indicates if the tokens that result from making a payment to the project
5 should be delivered claimed into the beneficiary's wallet, or unclaimed
6 to save gas.
7 @member preferAddToBalance A flag indicating if a distribution to a project
8 should prefer triggering its addToBalance function instead of its pay function.
9 @member percent The percent of the whole group that this split occupies.
10 This number is out of `JBConstants.SPLITS_TOTAL_PERCENT`.
11 @member projectId The ID of a project. If an allocator is not set but a
12 projectId is set, funds will be sent to the protocol treasury belonging
13 to the project whose ID is specified.
14 @member beneficiary An address. The role of the beneficiary depends on
15 whether or not projectId is specified, and whether or not an allocator
16 is specified.
17 @member lockedUntil Specifies if the split should be unchangeable until
18 the specified time, with the exception of extending the locked period.
19 @member allocator If an allocator is specified, funds will be sent to the
20 allocator contract along with all properties of this split.
21*/
22struct JBSplit {
23 bool preferClaimed;
24 bool preferAddToBalance;
25 uint256 percent;
26 uint256 projectId;
27 address payable beneficiary;
28 uint256 lockedUntil;
29 IJBSplitAllocator allocator;
30}

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 40+ protocols.

No spam. Unsubscribe anytime.

JBPrices

Manages and normalizes price feeds of various currencies.
The protocol uses this to allow projects to do their accounting in any number of currencies, but manage all funds in ETH or other assets regardless of accounting denomination.
feedFor property:
1// The available price feeds.
2// The feed returns the number of `_currency` units that can be
3// converted to 1 `_base` unit.
4
5mapping(uint256 => mapping(uint256 => IJBPriceFeed)) public override feedFor;
Step-by-step explanation of addFeedFor:
1function addFeedFor(
2 uint256 _currency,
3 uint256 _base,
4 IJBPriceFeed _feed
5) external override onlyOwner { ... }

Surface contracts

There are three main surface contracts that manage how projects manage funds and define how all core contracts should be used together.
  • JBController3_1 — stitches together funding cycles and project tokens, allowing for restricted control, accounting, and token management.
  • JBPayoutRedemptionPaymentTerminal3_1 — manages all inflows and outflows of funds.
  • JBSingleTokenPaymentTerminalStore3_1 — manages accounting data on behalf of payment terminals that manage balances of only one token type.

Juice Buyback Delegate

Juice buyback provides a data source and delegate which maximizes the project token received by the contributor when they call pay on the terminal.

What is a Buyback contract?

  • It allows a retailer to return unsold inventory up to a specified amount at an agreed-upon price.
  • It increases the optimal order quantity for the retailer, resulting in higher product availability and higher profits for both the retailer and the supplier.

What is a Data source contract?

  • A data source contract is a way of providing extensions to a treasury that either overrides or augments the default payout redemption payment functionality.
  • A data source contract can be used to provide custom data to the pay(...) transaction and/or the redeemTokensOf(...) transaction.
  • A data source is responsible for specifying any delegate hooks that should be triggered after the core functionality of a pay(...) or redeemTokensOf(...) transaction executes successfully.

What is a Delegate contract?

  • A delegate contract is a way of providing extensions to a treasury that augments the default payout redemption payment behavior.
  • Pay delegates include a custom didPay(...) hook that will execute after all of the default protocol pay logic has been successfully executed in the terminal contract.
  • Redemption delegates include a custom didRedeem(...) hook that will execute after all of the default protocol redeem logic has been successfully executed in the terminal contract.

JBXBuybackDelegate.sol

Datasource and delegate allow pay beneficiaries to get the highest amount of project tokens between minting using the project weight and swapping in a given Uniswap V3 pool.
Buyback delegate flow
Main external functions of the contract:

payParams()

Handles the data source implementation. Returns the weight to use, the original memo passed, and the amount to send to delegates instead of adding to the local balance.
1function payParams(JBPayParamsData calldata _data) external override
2 returns (
3 uint256 weight,
4 string memory memo,
5 JBPayDelegateAllocation[] memory delegateAllocations
6 )

didPay()

Delegates to either swap to the beneficiary or mint to the beneficiary.
This delegate is called only if the quote for the swap is bigger than the lowest received when minting.
1function didPay(JBDidPayData calldata _data) external payable override

uniswapV3SwapCallback()

Where the token transfer should happen.
1function uniswapV3SwapCallback(
2 int256 amount0Delta,
3 int256 amount1Delta,
4 bytes calldata data
5) external override

Hopefully, you can make use of this during the audit or even practice with the audit afterward.
Also, use this as an example of how you should prepare your audits.

Get in Touch

Preparing for a smart contract audit or running a bug bounty on an already deployed protocol? Knowing the architecture inside out is the first step to catching what others miss.
At Zealynx, we specialize in deep-dive security audits for DeFi protocols — from treasury management systems like Juicebox to complex DEX integrations.
Need an audit or a second pair of eyes on your deployed contracts? Get a quote or reach out directly to discuss your project.

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 40+ protocols.

No spam. Unsubscribe anytime.

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx