Skip to content

Bananapus/nana-core

Repository files navigation

Bananapus Core

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
  1. Usage
  2. Repository Layout
  3. Architecture
  4. Conceptual Overview
  5. Example Usage

Usage

Install

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.

Develop

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.

Scripts

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.

Deployments

With Sphinx

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.

Without Sphinx

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.

Tips

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.

Repository Layout

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.

Architecture

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]
Loading

Core Contracts

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.

Surface Contracts

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.

Utility Contracts

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.

Conceptual Overview

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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.
  3. Set itself (the controller being called) as the project's controller in the JBDirectory.
  4. Queue the first rulesets. More on this below.
  5. Set up any provided terminals as the project's terminals in the JBDirectory.

Rulesets

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.

Distributing Funds

Aside from redemptions (see Redemptions), funds can be accessed from a project's terminals in two ways: payouts or surplus allowance.

Payouts

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.

Surplus Allowance

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.

Payments, Tokens, and Redemptions

Juicebox project can receive funds in two ways:

  1. Funds can simply be added to a project's balance in a terminal with JBMultiTerminal.addToBalanceOf(…).
  2. 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:

  1. If the payment is not in the ruleset's base currency (JBRulesetMetadata.baseCurrency), use JBPrices 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 (like JBChainlinkV3PriceFeed) to convert the payment to USD.
  2. 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.
  3. 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 of JBConstants.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 by JBSplits (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.

Redemptions

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):

  1. With a 0% redemption rate, redemptions are turned off.
  2. With a 100% redemption rate, redemptions are 1:1 — somebody redeeming 10% of all project tokens will receive 10% of the surplus funds.
  3. 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:

$$f(x) = \frac{s \cdot x}{t} \times \left( r + \frac{x(1 - r)}{t} \right)$$

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.

Permissions

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

Hooks

"Hook" is a generic term for customizable contracts which "hook" into flows throughout the protocol and can be used to define custom functionality.

Ruleset Approval Hook

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.

Data Hooks

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.

Pay Hooks

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.

Redeem Hooks

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.

Split Hooks

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.

Fees

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%:

  1. Projects pay a 2.5% fee when they pay addresses with JBMultiTerminal.sendPayoutsOf(…) – payouts to other projects don't incur fees.
  2. Project owners pay a 2.5% fee when they use surplus allowance with JBMultiTerminal.useAllowanceOf(…).
  3. 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.

Held Fees

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(…).

Feeless Addresses

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.

Example Usage

For an explicit explanation, see the Conceptual Overview.

Launching a Project

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, the weight 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.

Paying a Project

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 a minReturnedTokens 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:

  1. Get the accounting context (see JBAccountingContext) to use for the payment.
  2. 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.
  3. Using the results from the terminal store, the terminal calls the mintTokensOf(…) function on project's controller, with useReservedRate set to true. The controller then calls JBTokens.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 $BING JBERC20 tokens in her wallet.
  4. 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.
  5. 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!

Sending Reserved Tokens and Payouts

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:

  1. 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.
  2. 30% go to Jeff's friend's wallet (0x456…). These tokens are sent directly to the wallet.
  3. 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:

  1. 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 in JBFundAccessLimits, calculates how much should be paid out based on the terminal's accounting context and price values reported by JBPrices, and updates the project's balance for the calling terminal.
  2. 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.
  3. 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.

Redeeming Tokens

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:

  1. Get the accounting context (see JBAccountingContext) to use for the redemption.
  2. 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.
  3. Using the results from the terminal store, the terminal calls the burnTokensOf(…) function on project's controller, which in turn calls JBTokens.burnFrom(…) to burn the tokens being redeemed.
  4. 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.
  5. 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.
  6. 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.