npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@bananapus/core

v0.0.38

Published

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.

Downloads

3,069

Readme

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.

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]

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

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.

| 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,