Current state of things
First thing's first: a huge thank you to everyone who has played with the first version of the Juicebox protocol over the past month. You've taken a leap of faith on a very experimental and untested set of contracts and user experiences with the hopes that it would help you smoothly realize your vision. The protocol has helped a number of projects bootstrap their treasury and community, and these communities have in turn helped Juicebox root into the fertile soil that is the Ethereum social layer.
I've been observing how each project has been interacting with the protocol. I've been a part of exciting discussions where JB was a total enabler of ideas and creativity, and also ones where I've unfortunately had to be the bearer of bad news that the protocol doesn't support the wild thought being proposed. I've seen people spin up projects and raise hundreds of ETH in hours, and seen people give up on the first screen because the "button" they were trying to click wasn't actually a button. After only a few weeks of action I have a sense of what's working, and I've got a laundry list of what could be better.
The goal is to steadily improve things over time. At the base contract layer however, progress must made in big leaps initially with the goal of eventually reaching a steady state as innovation moves to subsequent application layers. JuiceboxV2 is the first big leap. Its goal is simple: to enable more creativity, and remove all points of friction.
JuiceboxV1 was designed with the assumption that communities and project owners have adverse incentives. By using Juicebox, a project owner was committing to particular constraints so that their community could confidently embrace the finances of the game being proposed. Project owners could not mint or burn tokens at will, project owners could not dictate how many tokens were minted per ETH contributed, project owners could not limit who participated in a crowdfund, and project owners did not have a pause button.
Turns out this was a bad assumption to roll with at the base protocol layer. If a community and its owners are one and the same, flexibility is a requirement for total creative expression. It turns out that communities almost always crave a custom treasury strategy that fits their ethos and proposes a game that differentiates them from others.
Projects don't usually have the engineering resources to build, test, and verify such solutions though. This has been a core value Juicebox has provided for people, along with a simple and powerful UI for community members to join in through and follow along with. So far, the frictions that Juicebox removes has justified the treasury strategy constraints that it introduces.
Let's see if we can now do even better.
Proposed changes
Bring your own mint/burn strategy
You'll now be able to bring your own contracts that outline the game you want to propose to your community. You'll be able to plug and play with already-written strategies, or write your own custom one that fulfills your wildest ideas.
Writing a strategy can be simple, or as complex as you want. All that is required is providing a contract that adheres to the IFundingCycleDataSource
interface. You'll be able to provide a strategy that decides what happens when someone makes a payment to your project, as well as one for when someone redeems their treasury tokens.
Here's how writing a strategy around a payment works:
You can add a data source contract as a parameter to a funding cycle. Your data source must provide a function that implements the following function specification.
function payData(
address _payer,
uint256 _amount,
uint256 _baseWeight,
uint256 _reservedRate,
address _beneficiary,
string calldata _memo
)
external
returns (
uint256 weight,
string calldata memo,
IPayDelegate delegate
);
The function receives a handful of parameters from the Juicebox protocol, and is expected to return a handful of parameters back.
Inputs:
_payer
is the address that issued the ETH payment.
_amount
is the amount of the ETH payment received.
_baseWeight
is the weight of the funding cycle during which the payment is being made. This weight is determined by multiplying the previous cycle's weight by the previous cycle's discount rate. Each project's first funding cycle's weight is 10^24.
_reservedRate
is the reserved rate of the funding cycle during which the payment is being made. This percent is out of 200.
_beneficiary
is the address that the payer has specified to receive the resulting treasury tokens.
_memo
is the note that the payer has included in the payment.
Outputs:
-
weight
is the weight that the Juicebox protocol should use when minting treasury tokens. The total tokens minted will be amount
* weight
, where both variables are assumed to have 18 decimal places. Out of these minted tokens, some will be allocated to the _beneficiary
, and the rest will be reserved to be distributed to the reserved token recipients according to the _reservedRate
.
-
memo
is the memo to include with the protocol event that will be emitted as a result of the payment.
-
delegate
is the address of a contract that adheres to the IPaymentDelegate
interface. If a delegate
is provided, it will receive a callback from the Juicebox protocol once it has fully processed the payment. You can return the zero address if you don't need this functionality. The callback your delegate contract should implement is as follows:
function didPay(
address _payer,
uint256 _amount,
uint256 _weight,
uint256 _count,
address _beneficiary,
string calldata memo
) external;
-
_payer
is the same as the one passed in to your data source.
-
_amount
is the same as the one passed in to your data source.
-
_weight
is the same as the one returned from your data source.
-
_count
is the number of tokens that were minted for the _beneficiary
.
-
_beneficiary
is the same as the one passed in to your data source.
-
_memo
is the same as the one returned from your data source.
The recordPayment
function where all of these pieces come together can be found here.
A data source and delegate can similarly be provided to your funding cycle that'll shape the recordRedemption
function:
function redeemData(
address _holder,
uint256 _count,
uint256 _redemptionRate,
uint256 _ballotRedemptionRate,
address _beneficiary,
string calldata _memo
)
external
returns (
uint256 amount,
string calldata memo,
IRedeemDelegate delegate
);
Inputs:
_holder
is the token holder that is redeeming.
_count
is the number of tokens being redeemed.
_redemptionRate
is the redemption rate of the funding cycle during which the redemption is being made.
_ballotRedemptionRate
is the redemption rate that should be used if the project currently has an active funding cycle reconfiguration ballot.
_beneficiary
is the address that the redeemer has specified to claim the treasury ETH as a result of redeeming tokens.
_memo
is the note that the redeemer has included in the redemption.
Outputs:
-
amount
is the amount of ETH that should be sent from your treasury to the _beneficiary
as a result of redeeming/burning _count
tokens.
-
memo
is the memo to include with the protocol event that will be emitted as a result of the redemption.
-
delegate
is the address of a contract that adheres to the IRedemptionDelegate
interface. If a delegate
is provided, it will receive a callback from the Juicebox protocol once it has fully processed the redemption, but before the amount
is dispersed to the _beneficiary
. You can return the zero address if you don't want this functionality. The callback your delegate contract should implement is as follows:
function didRedeem(
address _holder,
uint256 _count,
uint256 _amount,
address _beneficiary,
string calldata memo
) external
-
_holder
is the same as the one passed in to your data source.
-
_count
is the same as the one passed in to your data source.
-
_amount
is the same as the one returned from your data source.
-
_beneficiary
is the same as the one passed in to your data source.
-
_memo
is the same as the one returned from your data source.
The recordRedemption
function where all of these pieces come together can be found here.
With these new tools projects can roll out all kinds of treasury strategies, such as:
- restricting payments to only certain addresses.
- restricting payments to only addresses that hold certain other assets.
- offering different levels of community membership depending on the state of the blockchain.
- restricting payments to be within min/max payment amounts.
- creating time weighted rewards.
- restricting the max supply of community tokens.
- customizing the amount of treasury tokens distributed per ETH received.
- minting NFTs for new members.
...or any combination of any of these, alongside any other rule you can express contractually.
Overflow allowance
Previously, a project could only access funds within its specified funding cycle target. All overflowed treasury funds over this target was only accessibly by treasury token holders.
Now, alongside specifying your funding cycle's target, you can specify an amount that you can use from your project's overflow on demand.
This is useful for allocating treasury funds for on-off use cases like bug-bounties, one-time contributions, audits, NFT bids, etc.
Open mint/burn
Previously, you could only mint tokens before receiving your first payment, and burning was only done through the redemption mechanism. All other tickets were distributed purely through the payment process according to funding cycle weights that decreased according to your configured discount rates over time.
You can now mint and allocate new treasury tokens at will. All token holders also now have the option to burn their tokens, for whatever reason.
This gives projects more flexibility to design their tokenomics the way they want, while also having an auto-distribution mechanism through Juicebox's flexible built-in payment mechanism alongside.
Reserved token distribution endpoints
Previously, payout splits could be directed at Ethereum addresses, other Juicebox projects, and arbitrary contracts that inherit from a common interface. Reserved tokens could only go to Ethereum addresses.
Now, reserved token distributions can also be pointed at Ethereum addresses, the owner of other Juicebox projects, and arbitrary contracts that inherit from this common interface.
This is useful to allow for more composable token distributions.
Pay, withdraw, and redeem can all be paused.
Previously, projects had not quick way to pause community interactions with its treasury.
Now, projects are able to individually pause function calls to pay, withdraw funds, and redeem tokens. These controls are configured into each funding cycle.
This gives projects quick levers to use to achieve certain treasury effects.
Adjustable fee
Previously, all projects paid a 5% fee from payouts.
Now, projects will pay at maximum a 5% fee that is adjustable by the JuiceboxDAO. There is also a ceiling fee price that is adjustable by the JuiceboxDAO.
This helps the JuiceboxDAO accommodate more projects and experiments into its ecosystem.
Conclusion
JuiceboxV2 introduces a suite of tools that allow for wild new treasury strategies. What remains constant from V1 is the fact that configurations are locked into funding cycles – if a project runs on 30 day funding cycles, they can specify creative dynamics into the funding cycle, but once the cycle begins changes can't be made until the following one. Also like V1, projects that opt for no duration are choosing the flexibility to make any change on demand.
The implementation of the new contracts is done, we've just now got to document, test, and audit everything. All code is public, as will be all documentation and conversation around this upgrade.
We need eyes and scrutiny. Please don't hesitate to take a look and help pick things apart. If you plan on spending time on this, please reach out to the DAO in our discord and introduce yourself so we can make sure you're rightly compensated for your work.
All projects currently running on Juicebox will be able to seamlessly migrate their treasury to V2.
LFG.