This repository contains the core protocol contracts for Bananapus' Juicebox v4. Juicebox is a flexible toolkit for launching and managing a treasury-backed token on Ethereum and L2s.
Table of Contents
How to install nana-core
in another project.
For projects using npm
to manage dependencies (recommended):
npm install @bananapus/core
For projects using forge
to manage dependencies (not recommended):
forge install Bananapus/nana-core
If you're using forge
to manage dependencies, add @bananapus/core/=lib/nana-core/
to remappings.txt
. You'll also need to install nana-core
's dependencies and add similar remappings for them.
nana-core
uses npm (version >=20.0.0) for package management and the Foundry development toolchain for builds, tests, and deployments. To get set up, install Node.js and install Foundry:
curl -L https://foundry.paradigm.xyz | sh
You can download and install dependencies with:
npm ci && forge install
If you run into trouble with forge install
, try using git submodule update --init --recursive
to ensure that nested submodules have been properly initialized.
Some useful commands:
Command | Description |
---|---|
forge build |
Compile the contracts and write artifacts to out . |
forge fmt |
Lint. |
forge test |
Run the tests. |
forge build --sizes |
Get contract sizes. |
forge coverage |
Generate a test coverage report. |
foundryup |
Update foundry. Run this periodically. |
forge clean |
Remove the build artifacts and cache directories. |
To learn more, visit the Foundry Book docs.
For convenience, several utility commands are available in package.json
.
Command | Description |
---|---|
npm test |
Run local tests. |
npm run test:fork |
Run fork tests (for use in CI). |
npm run coverage |
Generate an LCOV test coverage report. |
nana-core
manages deployments with Sphinx. To run the deployment scripts, install the npm devDependencies
with:
`npm ci --also=dev`
You'll also need to set up a .env
file based on .example.env
. Then run one of the following commands:
Command | Description |
---|---|
npm run deploy:mainnets |
Propose mainnet deployments. |
npm run deploy:testnets |
Propose testnet deployments. |
Your teammates can review and approve the proposed deployments in the Sphinx UI. Once approved, the deployments will be executed.
You can use the Sphinx CLI to run the deployment scripts without paying for Sphinx. First, install the npm devDependencies
with:
`npm ci --also=dev`
You can deploy the contracts like so:
PRIVATE_KEY="0x123..." RPC_ETHEREUM_SEPOLIA="https://rpc.ankr.com/eth_sepolia" npx sphinx deploy script/Deploy.s.sol --network ethereum_sepolia
This example deploys nana-core
to the Sepolia testnet using the specified private key. You can configure new networks in foundry.toml
.
To view test coverage, run npm run coverage
to generate an LCOV test report. You can use an extension like Coverage Gutters to view coverage in your editor.
If you're using Nomic Foundation's Solidity extension in VSCode, you may run into LSP errors because the extension cannot find dependencies outside of lib
. You can often fix this by running:
forge remappings >> remappings.txt
This makes the extension aware of default remappings.
The root directory contains this README, an MIT license, and config files.
The important source directories are:
nana-core/
├── script/ - Contains the forge + sphinx deploy script.
├── src/ - The Solidity source code for the contracts. Top level contains implementation contracts.
│ ├── abstract/ - Abstract utility contracts.
│ ├── enums/ - Enums.
│ ├── interfaces/ - Contract interfaces.
│ ├── libraries/ - Libraries.
│ └── structs/ - Structs.
└── test/ - Forge tests and testing utilities. Top level contains the main test files.
├── helpers/ - Generic helpers.
├── mock/ - Mocking utilities.
├── trees/ - Tree descriptions of unit test flows.
└── units/ - Unit tests.
Other directories:
nana-core/
├── .github/
│ └── workflows/ - CI/CD workflows.
├── deployments/ - Sphinx deployment logs.
└── utils/ - Miscellaneous utility scripts.
graph TD;
A[JBProjects] -->|Mints and tracks| B[Project NFTs]
B -->|Mapped by JBDirectory| C[JBMultiTerminal]
B -->|Mapped by JBDirectory| D[JBController]
C -->|Mints/burns on pay/redeem| D
C -->|Normalizes prices with| E[JBPrices]
C -->|Withdrawals restricted by| F[JBFundAccessLimits]
C -->|Stores balances and records transactions in| G[JBTerminalStore]
C -->|Payouts sent to| H[JBSplits]
D -->|Sends reserved tokens to| H[JBSplits]
D -->|Manages project rulesets in| I[JBRulesets]
I -->|Optionally checks next ruleset with| J[JBDeadline]
D -->|Manages credit/token balances in| K[JBTokens]
K -->|Optionally uses for accounting| L[JBERC20]
Contract | Description |
---|---|
JBProjects |
Stores project ownership and metadata. Projects are represented as ERC-721s. |
JBRulesets |
Manages rulesets and queuing for all projects. Rulesets dictate how a project behaves for a period of time. |
JBTokens |
Manages minting, burning, and balances of projects' tokens and token credits. |
JBPermissions |
Stores permissions for all addresses and operators. Addresses can give permissions to any other address (i.e. an operator) to execute specific Juicebox protocol operations on their behalf. |
JBDirectory |
Tracks the terminals and the controller that each project is using. |
JBFundAccessLimits |
Stores and manages payout limits and surplus allowance limits for each project, restricting the amount of funds each project can access from its terminals. |
JBPrices |
Manages and normalizes price feeds for use in terminals. Price feeds are contracts which return the "pricing currency" cost of 1 "unit currency". |
JBSplits |
Stores and manages splits for each project. Split groups are lists of wallets and projects which each receive a percent of a project's payouts or reserved tokens. |
The surface contracts are the entry points for external interactions with the Juicebox protocol, and define how the core contracts are used together. Anyone can write new surface contracts for projects to use.
Contract | Description |
---|---|
JBController |
Coordinates rulesets and project tokens, and is the entry point for most operations related to rulesets and project tokens. |
JBMultiTerminal |
Manages native/ERC-20 payments, redemptions, and surplus allowance usage for any number of projects. The entry point for operations involving inflows and outflows of funds. |
JBTerminalStore |
Manages bookkeeping for inflows and outflows of funds from any terminal addresses. |
A project's current controller and terminals can be found (or updated) through JBDirectory
.
Contract | Description |
---|---|
JBFeelessAddresses |
Stores a list of addresses that shouldn't incur fees. |
JBChainlinkV3PriceFeed |
An IJBPriceFeed implementation that reports prices from a Chainlink AggregatorV3Interface . |
JBDeadline |
A ruleset approval hook which rejects rulesets if they are not queued at least duration seconds before the current ruleset ends. In other words, rulesets must be queued before the deadline to take effect. |
JBERC20 |
An ERC-20 token which project have the option of using in JBTokens and JBController . |
If you prefer to learn by example, start with Example Usage.
Juicebox is a flexible toolkit for launching and managing a treasury-backed token on EVMs.
There are two main entry points for interacting with a Juicebox project:
- The project's terminals, which are the entry point for operations involving inflows and outflows of funds – payments, redemptions, payouts, and surplus allowance usage (more on this under Surplus Allowance). Each project can use multiple terminals, and a single deployed terminal can be used by many projects.
- The project's controller, which is the entry point for most operations related to a project's rulesets (more on this later) and its tokens.
nana-core
provides a trusted and well-understood implementation for each: JBMultiTerminal
is a generic terminal which manages payments, redemptions, payouts, and surplus allowance spending (more on this later) in native/ERC-20 tokens, and JBController
is a straightforward controller which coordinates rulesets (more on this under Rulesets) and project tokens. Projects can also bring their own terminals (which implement IJBTerminal
), or their own controllers (which implement IJBController
).
If the project's rules allow it, a project can migrate from one controller to another one with JBController.migrateController(…)
, or from one terminal to another with JBMultiTerminal.migrateBalanceOf(…)
.
JBDirectory
stores mappings of each project's current controller and terminals. It also stores their primary terminals – the primary terminal for a token is where payments in that token are routed to by default.
To launch a Juicebox project, any address can call JBController.launchProjectFor(…)
, which will:
- Mint the project's ERC-721 into the owner's wallet. Whoever owns this NFT is the project's owner, and has permission to manage the project's rules. These NFTs are stored in the
JBProjects
contract. - Store the project's metadata (if provided). This is typically an IPFS hash pointing to a JSON file with the project's name, description, and logo, but clients can use any metadata schema they'd like.
- Set itself (the controller being called) as the project's controller in the
JBDirectory
. - Queue the first rulesets. More on this below.
- Set up any provided terminals as the project's terminals in the
JBDirectory
.
The rules which dictate a project's behavior—including what happens when the project is paid, how its funds can be accessed, and how the project's rules can change in the future—are expressed as a queue of rulesets. A ruleset is a list of all the rules that currently apply to a project, which lasts for a pre-defined duration. The project's owner can add new rulesets to the end of the queue at any time by calling JBController.queueRulesetsOf(…)
.
Rulesets are stored and managed by the JBRulesets
contract, and are represented by the JBRuleset
and JBRulesetMetadata
structs. As mentioned above, the entry point for ruleset operations is the project's controller.
When a ruleset ends:
- If there are rulesets in the queue, the project will move on to the next one.
- If the ruleset queue is empty, the current ruleset keeps cycling (i.e. re-starting with the same duration).
As a special case, if the ruleset queue is empty AND the current ruleset has a duration of 0
, it lasts indefinitely. In this situation, when a new ruleset is queued, it will go into effect immediately.
Rulesets give project creators the ability to update their project's rules over time, and also allows them to offer supporters contractual guarantees about the project's future. A properly designed ruleset can guarantee refunds to supporters, or make certain kinds of rugpulls impossible.
Projects can further constrain their ability to change the project's rules with an approval hook (more on this under Hooks). This is a customizable contract attached to each ruleset, used to determine whether the next ruleset in the queue is approved to take effect. nana-core
provides the JBDeadline
approval hook, which rejects rulesets if they are not queued at least N
seconds before the current ruleset ends. In other words, rulesets must be queued before a deadline to take effect. JBDeadline
offers a clear window during which supporters can review upcoming changes and react to them (for example, by redeeming or selling their project tokens) before they are implemented.
Aside from redemptions (see Redemptions), funds can be accessed from a project's terminals in two ways: payouts or surplus allowance.
Payouts are the primary way a project can distribute funds from its terminals. Anyone can send a project's payouts with JBMultiTerminal.sendPayoutsOf(…)
, which pays out funds within the bounds of the ruleset's pre-defined payout limits:
Each ruleset is associated with a list of payout limits, which are stored in JBFundAccessLimits
. Each payout limit specifies an amount of funds that can be withdrawn from a project's terminals in terms of a specific currency. If a payout limit's currency is different from the currency used in a terminal, the amount of funds which can be paid out from that terminal varies depending on their exchange rate, as reported by JBPrices
. Payout limits can only be set by the project's controller, and are set when a ruleset is queued.
The sum of a ruleset's payout limits is the maximum amount of funds a project can pay out from its terminals during that ruleset. By default, all payouts go to the project's owner, but the owner can send payouts to multiple splits, which are other wallets or projects which will receive a percent of the payouts. Splits are stored and managed by JBSplits
– you can learn how splits are represented in the JBSplit
struct.
Splits are stored slightly differently from the project's other rules, and can be changed by the project's owner at any time (independently of the ruleset) with JBController.setSplitGroupsOf(…)
unless they are locked – splits have an optional lockedUntil
timestamp which prevents them from being changed until that time has passed.
By convention, a split group's groupId
for a given tokenAddress
is uint256(uint160(tokenAddress))
– the only exception to this is the reserved token split, which has a groupId
of 1.
Payout limits reset at the start of each ruleset – projects can use rulesets as a regular cadence for recurring payouts. If payouts are not sent out during a ruleset, the funds stay in the project's terminals.
Funds in excess of the payout limits are "surplus funds". Surplus funds stay in the terminals, serving as a runway for payouts in future rulesets. If the project has redemptions enabled, token holders can redeem their tokens to reclaim some of the surplus funds.
Project creators also have the option to withdraw surplus funds from a terminal with JBMultiTerminal.useAllowanceOf(…)
, which withdraws surplus funds for the beneficiary
specified by the project owner up to the pre-defined surplus allowance limit:
Like payout limits, surplus allowance limits are stored in JBFundAccessLimits
, and each surplus allowance limit specifies an amount of funds that can be withdrawn from a project's terminals in terms of a specific currency. Surplus allowance limits can only be set by the project's controller, and are set when a ruleset is queued.
Unlike payout limits, the surplus allowance does not reset at the start of each ruleset – once the surplus allowance is used, a new surplus allowance must be initialized by the controller before further surplus can be withdrawn. Surplus allowance is sometimes used for discretionary spending. Most projects use a surplus allowance of 0
, meaning the owner cannot withdraw surplus funds.
Juicebox project can receive funds in two ways:
- Funds can simply be added to a project's balance in a terminal with
JBMultiTerminal.addToBalanceOf(…)
. - More often, a project is paid with
JBMultiTerminal.pay(…)
, minting credits or tokens for the payer or a beneficiary they specify.
By default, the JBTokens
contract tracks credit balances for a project's payers. Credits are a simple accounting mechanism – they can be transferred with JBController.transferCreditsFrom(…)
, or redeemed with JBMultiTerminal.redeemTokensOf(…)
(redeeming reclaims some funds from the project's terminal – read more under Redemptions).
A project's creator can call JBController.deployERC20For(…)
to deploy a JBERC20
token for their project which can be traded on exchanges, used for on-chain voting (JBERC20
implements ERC20Votes
), and more. From then on, JBTokens
will track balances in both credits and the ERC-20 token. Credit holders can use JBController.claimTokensFor(…)
to exchange their credits for the ERC-20 tokens, and like credits, tokens can be redeemed with JBMultiTerminal.redeemTokensOf(…)
.
Project creators can bring their own token as long as it implements IJBToken
, and set it as their project's token using JBController.setTokenFor(…)
.
When someone pays a project, the number of credits or tokens they receive is determined by the ruleset:
- If the payment is not in the ruleset's base currency (
JBRulesetMetadata.baseCurrency
), useJBPrices
to figure out how much the payment is worth in terms of the base currency. For example, if the base currency is USD and the payment is in ETH, use an ETH/USD price feed (likeJBChainlinkV3PriceFeed
) to convert the payment to USD. - The value, now expressed in terms of the base currency, gets multiplied by the ruleset's weight (
JBRuleset.weight
) to determine the number of credits or tokens that will be minted. - The number of credits or tokens the beneficiary receives is then reduced by the ruleset's reserved rate (
JBRulesetMetadata.reservedRate
expressed as a fraction out ofJBConstants.MAX_RESERVED_RATE
). For example, if the reserved rate is 20%, the beneficiary will receive 80% of the tokens minted by the payment. The remaining 20% is reserved for a list of reserved splits which are managed byJBSplits
(just like payout splits).
If the ruleset queue is empty and a ruleset is cycling (re-starting each time it ends), the ruleset's decay rate (JBRuleset.decayRate
, expressed as a fraction out of JBConstants.MAX_DECAY_RATE
) automatically reduces the weight each cycle. With a 5% decay rate, the weight gets reduced to 95% of its initial value in the second cycle, 90.25% in the third, and so on. This can be used to reward early supporters for taking on more risk without the need to manually queue new cycles.
Credits and project tokens can be redeemed to reclaim some funds from the treasury with JBMultiTerminal.redeemTokensOf(…)
. Only funds not being used for payouts (surplus funds) are available for redemption, so if a project's combined payout limits exceed its combined terminal balances, it can't be redeemed from. Redemptions are influenced by the ruleset's redemption rate (JBRulesetMetadata.redemptionRate
, expressed as a fraction out of JBConstants.MAX_REDEMPTION_RATE
):
- With a 0% redemption rate, redemptions are turned off.
- With a 100% redemption rate, redemptions are 1:1 — somebody redeeming 10% of all project tokens will receive 10% of the surplus funds.
- Between 0% and 100%, redemptions are scaled down by the redemption rate. For example, with a 50% redemption rate, somebody redeeming 10% of all project tokens will receive about
$10% \times 50% = 5%$ of the surplus funds. The other ~5% stays in the project, increasing the redemption value of everyone else's tokens (because the ratio of surplus to tokens has increased).
The lower the redemption rate, the more of an incentive for token holders to redeem later than others – earlier redeemers receive less of the surplus than later ones. The majority of projects use a redemption rate of 0% (redemptions disabled) or 100% (1:1 redemptions).
Redemption Bonding Curve
With a 50% redemption rate, somebody redeeming 10% of all project tokens technically receives slightly more than 5% of the surplus funds. This is because a redemption rate between 0% and 100% enables redemptions along a bonding curve. Specifically, the formula is:
Where:
-
$f(x)$ is the amount of funds reclaimed by redeeming$x$ tokens, -
$r$ is the redemption rate (from 0 to 1), -
$s$ is the amount of surplus funds (the funds available for redemption), and -
$t$ is the current token supply.
JBPermissions
allows one address to grant another address permission to call functions in Juicebox contracts on their behalf. Each ID in JBPermissionIds
grants access to a specific set of these functions, which can be granted to another address with JBPermissions.setPermissionsFor(…)
.
For example, if alice.eth
owns project ID #5, she can queue new rulesets for the project. If alice.eth
gives bob.eth
permission to QUEUE_RULESETS
, bob.eth
can also queue rulesets for project ID #5.
Permission IDs
ID | Name | Description | Used By |
---|---|---|---|
1 | ROOT |
All permissions across every contract. Very dangerous. | |
2 | QUEUE_RULESETS |
Permission to call JBController.queueRulesetsOf and JBController.launchRulesetsFor . |
nana-core |
3 | REDEEM_TOKENS |
Permission to call JBMultiTerminal.redeemTokensOf . |
nana-core |
4 | SEND_PAYOUTS |
Permission to call JBMultiTerminal.sendPayoutsOf . |
nana-core |
5 | MIGRATE_TERMINAL |
Permission to call JBMultiTerminal.migrateBalanceOf . |
nana-core |
6 | SET_PROJECT_URI |
Permission to call JBController.setUriOf . |
nana-core |
7 | DEPLOY_ERC20 |
Permission to call JBController.deployERC20For . |
nana-core |
8 | SET_TOKEN |
Permission to call JBController.setTokenFor . |
nana-core |
9 | MINT_TOKENS |
Permission to call JBController.mintTokensOf . |
nana-core |
10 | BURN_TOKENS |
Permission to call JBController.burnTokensOf . |
nana-core |
11 | CLAIM_TOKENS |
Permission to call JBController.claimTokensFor . |
nana-core |
12 | TRANSFER_CREDITS |
Permission to call JBController.transferCreditsFrom . |
nana-core |
13 | SET_CONTROLLER |
Permission to call JBDirectory.setControllerOf . |
nana-core |
14 | SET_TERMINALS |
Permission to call JBDirectory.setTerminalsOf . |
nana-core |
15 | SET_PRIMARY_TERMINAL |
Permission to call JBDirectory.setPrimaryTerminalOf . |
nana-core |
16 | USE_ALLOWANCE |
Permission to call JBMultiTerminal.useAllowanceOf . |
nana-core |
17 | SET_SPLIT_GROUPS |
Permission to call JBController.setSplitGroupsOf . |
nana-core |
18 | ADD_PRICE_FEED |
Permission to call JBPrices.addPriceFeedFor . |
nana-core |
19 | ADD_ACCOUNTING_CONTEXTS |
Permission to call JBMultiTerminal.addAccountingContextsFor . |
nana-core |
20 | ADJUST_721_TIERS |
Permission to call JB721TiersHook.adjustTiers . |
nana-721-hook |
21 | SET_721_METADATA |
Permission to call JB721TiersHook.setMetadata . |
nana-721-hook |
22 | MINT_721 |
Permission to call JB721TiersHook.mintFor . |
nana-721-hook |
23 | SET_721_DISCOUNT_PERCENT |
Permission to call JB721TiersHook.setDiscountPercentOf . |
nana-721-hook |
24 | SET_BUYBACK_TWAP |
Permission to call JBBuybackHook.setTwapWindowOf and JBBuybackHook.setTwapSlippageToleranceOf . |
nana-buyback-hook |
25 | SET_BUYBACK_POOL |
Permission to call JBBuybackHook.setPoolFor . |
nana-buyback-hook |
26 | ADD_SWAP_TERMINAL_POOL |
Permission to call JBSwapTerminal.addDefaultPool . |
nana-swap-terminal |
27 | ADD_SWAP_TERMINAL_TWAP_PARAMS |
Permission to call JBSwapTerminal.addTwapParamsFor . |
nana-swap-terminal |
28 | MAP_SUCKER_TOKEN |
Permission to call BPSucker.mapToken . |
nana-suckers |
29 | DEPLOY_SUCKERS |
Permission to call BPSuckerRegistry.deploySuckersFor . |
nana-suckers |
"Hook" is a generic term for customizable contracts which "hook" into flows throughout the protocol and can be used to define custom functionality.
Each ruleset can have a ruleset approval hook (an IJBRulesetApprovalHook
under JBRuleset.approvalHook
). Before the next ruleset in the queue goes into effect, it must be approved by the current ruleset's ruleset approval hook. If the ruleset approval hook rejects the next ruleset, the next ruleset does not go into effect and the current ruleset keeps cycling.
nana-core
provides the JBDeadline
ruleset approval hook, described under Rulesets, and project creators can bring their own ruleset approval hooks to enforce custom rules for whether rulesets can take effect.
Each ruleset can have a data hook (an IJBRulesetDataHook
under JBRulesetMetadata.dataHook
) which extends any terminal's payment or redemption functionality by overriding the original weight or memo based on custom logic. Data hooks can also specify pay or redeem hooks for the terminal to fulfill, or allow addresses to mint a project's tokens on-demand.
The ruleset's data hook is called by the terminal upon payments if JBRulesetMetadata.useDataHookForPay
is true, and upon redemptions if JBRulesetMetadata.useDataHookForRedeem
is true. Data hooks operate before the payment or redemption is recorded in the JBTerminalStore
.
The data hook can return one or more pay hooks for the terminal to call after its pay(…)
logic completes and has been recorded in the JBTerminalStore
. Pay hooks implement IJBPayHook
, and can be used to implement custom logic triggered by payments.
A common pattern is for a single contract to be both a data hook and a pay hook. JB721TiersHook
(from nana-721-hook
) and JBBuybackHook
(from nana-buyback-hook
) are both examples of this.
The data hook can return one or more redeem hooks for the terminal to call after its redeemTokensOf(…)
logic completes and has been recorded in the JBTerminalStore
. Redeem hooks implement IJBRedeemHook
, and can be used to implement custom logic triggered by redemptions.
Like pay hooks, a single contract can be a data hook and a redeem hook. JB721TiersHook
(from nana-721-hook
) is a data hook, a pay hook, and a redeem hook.
Each split can have a split hook (an IJBSplitHook
under JBSplit.splitHook
) which defines custom logic, triggered when a terminal is processing a payout to that split – the terminal optimistically transfers the tokens to the split and calls its processSplitWith(…)
function.
Terminals have the option to charge fees. JBMultiTerminal
charges a 2.5% fee on payouts to addresses, surplus allowance usage, and redemptions if the redemption rate is less than 100%:
- Projects pay a 2.5% fee when they pay addresses with
JBMultiTerminal.sendPayoutsOf(…)
– payouts to other projects don't incur fees. - Project owners pay a 2.5% fee when they use surplus allowance with
JBMultiTerminal.useAllowanceOf(…)
. - If the redemption rate is not 100%, redeemers pay a 2.5% fee on redemptions through
JBMultiTerminal.redeemTokensOf(…)
.
JBMultiTerminal
sends fees to Project ID #1, which is the first project launched during the deployment process.
If a ruleset has JBRulesetMetadata.holdFees
set to true, JBMultiTerminal
will not immediately pay the fees to project #1. Instead, the fees will be held in the terminal, and can be unlocked (returned to the project's balance) by adding the same amount of funds that incurred the fees back to the project's balance (by calling JBMultiTerminal.addToBalanceOf(…)
with shouldReturnHeldFees
set to true). Held fees are "safe" for 28 days – after that time, anyone can process them by calling JBMultiTerminal.processHeldFeesOf(…)
.
JBFeelessAddresses
manages a list of addresses which are exempt from fees. Feeless addresses can receive payouts, use surplus allowance, or be the beneficiary of redemptions without incurring fees. Only the contract's owner can add or remove feeless addresses.
For an explicit explanation, see the Conceptual Overview.
Jeff wants to raise funds for his startup, "Bingle". He decides to launch a Bingle Juicebox project on Ethereum mainnet. To launch his project, he calls JBController.launchProjectFor(…)
, passing the following arguments:
Param | Value | Why |
---|---|---|
owner |
0x765… (bingle.eth ) |
This is the Bingle multisig, which Jeff wants to use to safely manage the project. |
projectUri |
QmQHGuXv7nDh1rxj48HnzFtwvVxwF1KU9AfB6HbfG8fmJF |
This IPFS hash points to a JSON file with the Bingle metadata. |
rulesetConfigurations |
[…] |
More below. |
terminalConfigurations |
[…] |
More below. |
memo |
"Bingle is the best startup in the world." |
This memo is included in the event emitted by the project's launch. |
His rulesetConfigurations
array only contains a single ruleset, and looks like this:
[
{
// Jeff's ruleset takes effect immediately.
mustStartAtOrAfter: 1,
// The ruleset lasts for a week (which is 604,800 seconds).
duration: 604_800,
// The ruleset mints 100 tokens (with 18 decimals) per unit of payment.
weight: 100_000_000_000_000_000_000,
// The weight decays by 10% each cycle. Calculated out of `JBConstants.MAX_DECAY_RATE` (1e9).
decayRate: 100_000_000,
// This is the address of the `JBDeadline` approval hook with a 24 hour duration (86,400 seconds).
approvalHook: "0x123…",
metadata: {
// Jeff reserves 30% of the tokens minted while this ruleset is active.
// Calculated out of `JBConstants.MAX_RESERVED_RATE` (1e4).
reservedRate: 3_000,
// Jeff allows 1:1 redemptions for the tokens minted while this ruleset is active.
// Calculated out of `JBConstants.MAX_REDEMPTION_RATE` (1e4).
redemptionRate: 10_000,
// Jeff uses `JBConstants.NATIVE_TOKEN` (ETH) as the base currency.
// By convention, token currencies are represented by `uint32(uint160(tokenAddress))`.
baseCurrency: uint32(
uint160("0x000000000000000000000000000000000000EEEe")
),
// Jeff allows payments to the project.
pausePay: false,
// Jeff allows payers to transfer their credits.
pauseCreditTransfers: false,
// Jeff doesn't allow the Bingle multisig to mint credits/tokens on demand.
allowOwnerMinting: false,
// Jeff doesn't allow the Bingle multisig to migrate or set terminals or controllers during the ruleset.
allowTerminalMigration: false,
allowSetTerminals: false,
allowControllerMigration: false,
allowSetController: false,
// Jeff pays fees when they're incurred.
holdFees: false,
// Bingle credit/token holders can redeem from the project's total surplus across all terminals,
// and not just the local terminal surplus.
useTotalSurplusForRedemptions: true,
// The Bingle project doesn't use a data hook for payments or redemptions.
useDataHookForPay: false,
useDataHookForRedeem: false,
dataHook: "0x0000000000000000000000000000000000000000",
// This ruleset doesn't need any metadata.
metadata: 0,
},
splitGroups: [
{
// This ID comes from `JBSplitGroupIds.RESERVED_TOKENS`. This group is for reserved tokens.
// By convention, *payout* split groups use `uint256(uint160(tokenAddress))` as a `groupId`.
groupId: 1,
splits: [
{
// Typically used for payouts to projects. If true, it uses `addToBalanceOf(…)`.
// If false, it will `pay(…)` the project.
preferAddToBalance: false,
// 25% of `JBConstants.SPLITS_TOTAL_PERCENT` (1e9).
percent: 250_000_000,
// This split is paid to project #5, which helps Bingle with marketing.
projectId: 5,
// Any tokens minted by this split's payment go to Jeff's friend (with wallet 0x456…).
beneficiary: "0x456…",
// This split can be changed by the Bingle multisig at any time.
lockedUntil: 0,
// This split doesn't use a split hook.
hook: "0x0000000000000000000000000000000000000000",
},
{
preferAddToBalance: false,
// 30% of `JBConstants.SPLITS_TOTAL_PERCENT` (1e9).
percent: 300_000_000,
// This split is paid directly to the `beneficiary` address, not a project.
projectId: 0,
// This is Jeff's friend, who helped him set up the project.
beneficiary: "0x456…",
// This split can be changed by the Bingle multisig at any time.
lockedUntil: 0,
// This split doesn't use a split hook.
hook: "0x0000000000000000000000000000000000000000",
},
],
},
],
fundAccessLimitGroups: [
{
// This is the address of `JBMultiTerminal`, which the Bingle project uses to manage payouts.
terminal: "0x789…",
// These limits determine how much ETH (`JBConstants.NATIVE_TOKEN`) can be paid out from the terminal.
token: "0x000000000000000000000000000000000000EEEe",
payoutLimits: [
{
// 1 (with 18 decimals).
amount: 1_000_000_000_000_000_000,
// Jeff uses `JBConstants.NATIVE_TOKEN` (ETH) as the payout currency.
// By convention, token currencies are represented by `uint32(uint160(tokenAddress))`.
currency: uint32(
uint160("0x000000000000000000000000000000000000EEEe")
),
},
],
surplusAllowances: [], // Jeff doesn't allow any surplus allowance usage.
},
],
},
];
Some things to note:
- Jeff only sets up a single ruleset, which takes effect immediately and lasts for a week. Unless he queues another ruleset at least 24 hours before the end of this ruleset (as required by the
JBDeadline
approval hook), this ruleset will cycle indefinitely. Each time it cycles, theweight
will decay by 10%. - Jeff only specified a single split group, which is for reserved tokens. 25% of the tokens minted while the ruleset is active to project #5, and 30% go to his friend's wallet. The remaining 45% go to the project's owner (the Bingle multisig).
- Jeff set up a single fund access limit group for
JBMultiTerminal
. This group restricts payouts to 1 ETH per ruleset, but this resets when the ruleset cycles over. Jeff didn't set up any surplus allowance limits, so he can't withdraw surplus funds from the terminal. Since Jeff didn't specify any split groups for payouts, all payouts go to the project's owner (the Bingle multisig).
For a detailed description of the fields in the structs above, see the natspec documentation for JBRulesetConfig
, JBRulesetMetadata
, JBSplitGroup
, JBSplit
, JBFundAccessLimitGroup
, and JBCurrencyAmount
.
His terminalConfigurations
array sets up two terminals. The first is the JBMultiTerminal
, which the Bingle project uses to accept ETH payouts, make redemptions available, and manage payouts. The second terminal is the JBSwapTerminal
, which the Bingle project uses to accept USDC and convert them to ETH on payment (for a more detailed explanation, see nana-swap-terminal
). The terminalConfigurations
look like this:
[
{
terminal: "0x789…", // This is the address of `JBMultiTerminal`.
tokensToAccept: ["0x000000000000000000000000000000000000EEEe"], // The Bingle project accepts ETH (`JBConstants.NATIVE_TOKEN`) through this terminal.
},
{
terminal: "0xABC…", // This is the address of `JBSwapTerminal`.
tokensToAccept: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], // The Bingle project accepts USDC through the swap terminal.
},
];
Note that Jeff didn't have to set his controller – the controller he calls launchProjectFor(…)
on sets itself as the project's controller in the JBDirectory
.
The controller also mints the JBProjects
ERC-721 which represents the project into the Bingle multisig's wallet, stores the project's metadata, queues the first ruleset, and sets up the terminals in the directory. The Bingle project is now live on Ethereum mainnet!
The Bingle multisig wants their project's token to be an ERC-20, so they call JBController.deployERC20For(…)
to deploy the $BING token, which is a JBERC20
contract. From now on, the JBTokens
contract will automatically mint $BING ERC-20 tokens for payers.
Stacy wants to support Bingle and get $BING tokens, so she decides to pay Jeff's project 1 ETH. She calls JBMultiTerminal.pay(…)
, passing the following arguments:
Parameter | Value | Explanation |
---|---|---|
projectId |
6 |
This is the Bingle project's ID from JBProjects |
token |
0x000000000000000000000000000000000000EEEe |
Stacy is paying with ETH, represented by JBConstants.NATIVE_TOKEN . |
amount |
1000000000000000000 |
1 ETH, with 18 decimals. |
beneficiary |
0x379… (stacy.eth ) |
Stacy wants to receive the tokens minted by her payment. |
minReturnedTokens |
70000000000000000000 |
Stacy expects to receive 70 tokens (with 18 decimals). |
memo |
"Bingle rocks." |
This memo is included in the event emitted by the payment. |
metadata |
0x |
Metadata can be used to control custom features in hooks or terminals, but there's no need here. |
Since Stacy is paying with the native token (ETH), she has to include 1 ETH as her transaction's msg.value
. Stacy expects to receive 70 tokens because:
- The ruleset has a weight of
100000000000000000000
(100 with 18 decimals), meaning the Bingle project mints 100 $BING per ETH paid. - The ruleset has a 30% reserved rate, meaning 30 of the 100 $BING tokens minted are set aside for the reserved split group (
JBSplitGroupIds.RESERVED_TOKENS
). Stacy will get the 70 remaining $BING tokens. Because Stacy specified aminReturnedTokens
of 70 tokens, the payment will revert if she doesn't receive at least that many tokens for some reason.
Once pay(…)
is called, the Bingle project's terminal will:
- Get the accounting context (see
JBAccountingContext
) to use for the payment. - Record the payment in the terminal's store by calling
JBTerminalStore.recordPaymentFrom(…)
. The terminal store uses the ruleset's weight and the accounting context to calculate how many $BING tokens should be minted. It also checks whether the payment should use any hooks by checking the ruleset's data hook – the Bingle project has no data hooks, so the payment proceeds. - Using the results from the terminal store, the terminal calls the
mintTokensOf(…)
function on project's controller, withuseReservedRate
set to true. The controller then callsJBTokens.mintFor(…)
to mint 70 tokens for the beneficiary (stacy.eth
) and sets aside 30 tokens as "pending reserved tokens" (more on this below). Since the Bingle multisig deployed the $BING token, Stacy receives 70 $BINGJBERC20
tokens in her wallet. - If there were a data hook, and that data hook had returned pay hooks for the terminal to use, the terminal would call their
afterPayRecordedWith(…)
functions here. Since this payment didn't use any hooks, the payment proceeds. - The terminal emits a
Pay
event with the payment details.
Now the Bingle project has a balance of 1 ETH in its JBMultiTerminal
, and Stacy has 70 $BING tokens in her wallet!
Now that there are 30 reserved $BING tokens pending and 1 ETH in the Bingle project's terminal, Jeff decides the payouts and reserved tokens should be sent. Even though Jeff doesn't own the project (the Bingle multisig does), he can still send payouts and reserved tokens because they are public functions.
To mint and send out the pending reserved tokens, Jeff calls JBController.sendReservedTokensToSplitsOf(…)
. When this function is called, the controller mints the 30 pending reserved tokens and sends them out to the reserved token split group.
The 30 $BING tokens are minted and divided between the splits according to the rules described in JBSplit
. As a reminder, Jeff originally set up 2 reserved token splits:
- 25% of the reserved tokens go to project #5. If project #5 had a payment terminal which accepted $BING, these tokens would be paid into that terminal. Since it doesn't, the tokens are sent to the wallet which owns project #5.
- 30% go to Jeff's friend's wallet (
0x456…
). These tokens are sent directly to the wallet. - Since the splits don't add up to 100%, the remaining 45% go to the Bingle project's current owner, the Bingle multisig. If the Bingle project was transferred to a new owner, the reserved tokens would go to the new owner instead.
To send the payouts, Jeff calls JBMultiTerminal.sendPayoutsOf(…)
. When he does, the terminal:
- Records the payment in the terminal store, calling
JBTerminalStore.recordPayoutFor(…)
. In this function, the terminal store makes sure the payout doesn't exceed the project's payout limits inJBFundAccessLimits
, calculates how much should be paid out based on the terminal's accounting context and price values reported byJBPrices
, and updates the project's balance for the calling terminal. - Next, the terminal sends payouts to each split in the payout split group for the token (while following the rules described in
JBSplit
). While doing this, the terminal calculates how much of what was paid out was eligible for fees. Since Jeff didn't set up any payout splits, the terminal proceesds. - Any funds that weren't paid out to splits are sent to the project's owner, which is the Bingle multisig.
JBMultiTerminal
charges a 2.5% fee on payouts to wallets, and 1 ETH is being paid to a wallet (the Bingle multisig), so 0.025 ETH is paid to project #1 (see Fees). If project #1 has a terminal which accepts ETH, this payment mints tokens from project #1 – those tokens are sent to the owner of the project paying the fees, which is the Bingle multisig.
At the end of this:
- The Bingle project has 0 ETH in its terminal.
- The Bingle project used its entire 1 ETH payout limit, so it can't send any more payouts until the ruleset ends. It can still receive payments.
- The Bingle multisig received 0.975 ETH, and 0.025 ETH's worth of tokens from project #1.
After a few hours, Stacy decides she wants to redeem her 70 $BING tokens to get back some ETH. She calls JBMultiTerminal.redeemTokensOf(…)
to redeem all of her $BING tokens, passing her own address as the beneficiary.
Redemptions are calculated based on:
- The number of tokens being redeemed (70 $BING).
- The total token supply. Let's say that a few more payments have come in, and there are now 700 $BING tokens in circulation. This means Stacy is redeeming 10% of the $BING supply.
- The project's balance. 7 ETH was paid in to mint the 700 $BING tokens, but 1 ETH was paid out, so the project's balance is 6 ETH.
- The ruleset's redemption rate, which is 100% for the Bingle project. This means that 10% of the $BING token can be redeemed for 10% of the balance.
This means that Stacy will receive 0.6 ETH by redeeming her 70 $BING tokens. When she calls the function, the terminal will:
- Get the accounting context (see
JBAccountingContext
) to use for the redemption. - Record the redemption in the terminal's store by calling
JBTerminalStore.recordRedemptionFor(…)
. The terminal store uses the current surplus (see Payouts) and the numbers mentioned above to calculate how much ETH should be returned. It also checks whether the redemption should use any hooks by checking the ruleset's data hook – the Bingle project has no data hooks, so the redemption proceeds. - Using the results from the terminal store, the terminal calls the
burnTokensOf(…)
function on project's controller, which in turn callsJBTokens.burnFrom(…)
to burn the tokens being redeemed. - If the redemption rate is less than 100%,
JBMultiTerminal
would take a 2.5% fee from the amount being redeemed. Since the Bingle project has a 100% redemption rate, Stacy doesn't pay any fees. - If there were a data hook, and that data hook had returned redeem hooks for the terminal to use, the terminal would call their
afterRedeemRecordedWith(…)
functions here. Since this redemption didn't use any hooks, the redemption proceeds. - The terminal emits a
RedeemTokens
event with the redemption details.
Once the transaction has finished:
- Stacy receives 0.6 ETH in her wallet.
- Stacy has 0 $BING tokens in her wallet.
- The Bingle project has 5.4 ETH left in its terminal (which is all surplus).
- The total $BING supply is down to 630 tokens – when tokens are redeemed, they're burned and taken out of circulation.
See Redemptions for more details on how redemptions are calculated.